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

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Firefox support with webextension-polyfill

Implemented cross-browser compatibility for the extension:

- Installed webextension-polyfill for unified browser.* API
- Replaced all chrome.* API calls with browser.* imports
- Updated build system to output both chrome/ and firefox/ directories
- Created Firefox-specific manifest with browser_specific_settings

byarielm.fyi be9f4207 603cf0a1

verified
+602 -67
+18 -10
docs/git-history.json
··· 1 1 [ 2 2 { 3 + "hash": "603cf0a187850664336a12c9e5cbb49038906f53", 4 + "short_hash": "603cf0a", 5 + "author": "Ariel M. Lighty", 6 + "date": "2025-12-27T22:42:43-05:00", 7 + "message": "fix: CORS for extension credentialed requests\n\nUpdated CORS headers to support credentials from Chrome extensions:\n- Added getCorsHeaders() to detect chrome-extension:// origins\n- Changed from wildcard Access-Control-Allow-Origin to specific origin\n- Added Access-Control-Allow-Credentials: true for credentialed requests\n- Updated session endpoint to pass event for CORS header detection", 8 + "files_changed": 4 9 + }, 10 + { 3 11 "hash": "bd3aabb75abb1875aef125610fcdccb14967a8e3", 4 12 "short_hash": "bd3aabb", 5 13 "author": "Ariel M. Lighty", ··· 8 16 "files_changed": 5 9 17 }, 10 18 { 11 - "hash": "d07180cd3a19328b82b35118e525b59d4e2e060b", 12 - "short_hash": "d07180c", 19 + "hash": "bd3aabb75abb1875aef125610fcdccb14967a8e3", 20 + "short_hash": "bd3aabb", 13 21 "author": "Ariel M. Lighty", 14 - "date": "2025-12-27T18:38:39-05:00", 15 - "message": "feat: add Tailwind CSS to extension\n\nReplaced 299 lines of vanilla CSS with Tailwind for design consistency with web app. Production build minified to 13KB.", 16 - "files_changed": 9 22 + "date": "2025-12-27T22:10:11-05:00", 23 + "message": "fix: extension dark mode and build mode messaging\n\n- Changed darkMode from 'class' to 'media' for automatic system preference detection\n- Made server offline message conditional on build mode (dev vs prod)\n- Hide dev server instructions in production builds", 24 + "files_changed": 5 17 25 }, 18 26 { 19 27 "hash": "d07180cd3a19328b82b35118e525b59d4e2e060b", ··· 32 40 "files_changed": 6 33 41 }, 34 42 { 35 - "hash": "fcf682bb8969aca108262348e7e17531077713be", 36 - "short_hash": "fcf682b", 43 + "hash": "2a163c5f033a79324b100a236ea26c905909bfc6", 44 + "short_hash": "2a163c5", 37 45 "author": "Ariel M. Lighty", 38 - "date": "2025-12-27T15:48:44-05:00", 39 - "message": "docs: improve decision graph workflow with lifecycle management\n\nUpdated CLAUDE.md with comprehensive node lifecycle management:\n- Added node status transitions (pending → in_progress → completed)\n- Correct orphan detection commands (awk instead of cut)\n- Common mistakes section with examples\n- Enhanced audit checklist with status verification\n- Verification workflow after node creation\n\nAlso updated extension popup with ATmosphere branding.\n\nDecision graph now at 331 nodes, 332 edges - all orphans resolved.", 40 - "files_changed": 4 46 + "date": "2025-12-27T15:49:08-05:00", 47 + "message": "docs: update decision graph after documentation improvements", 48 + "files_changed": 2 41 49 }, 42 50 { 43 51 "hash": "fcf682bb8969aca108262348e7e17531077713be",
+231
docs/graph-data.json
··· 4014 4014 "created_at": "2025-12-27T22:41:38.430661200-05:00", 4015 4015 "updated_at": "2025-12-27T22:41:48.981429600-05:00", 4016 4016 "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4017 + }, 4018 + { 4019 + "id": 366, 4020 + "change_id": "77b7ed7e-a113-41f6-a677-50d376f3f008", 4021 + "node_type": "outcome", 4022 + "title": "Committed CORS fixes to git", 4023 + "description": null, 4024 + "status": "completed", 4025 + "created_at": "2025-12-27T22:42:49.037783-05:00", 4026 + "updated_at": "2025-12-27T22:42:54.162857-05:00", 4027 + "metadata_json": "{\"branch\":\"master\",\"commit\":\"603cf0a\",\"confidence\":95}" 4028 + }, 4029 + { 4030 + "id": 367, 4031 + "change_id": "df6abf7a-e7a4-45f3-8485-b933319416d9", 4032 + "node_type": "goal", 4033 + "title": "Create Firefox-compatible version of Twitter scraper extension", 4034 + "description": null, 4035 + "status": "pending", 4036 + "created_at": "2025-12-28T18:09:33.241860800-05:00", 4037 + "updated_at": "2025-12-28T18:09:33.241860800-05:00", 4038 + "metadata_json": "{\"branch\":\"master\",\"confidence\":85,\"prompt\":\"let's make the extension have a firefox compatible version too.\"}" 4039 + }, 4040 + { 4041 + "id": 368, 4042 + "change_id": "79721edf-aa05-4580-8c28-7d20941ef155", 4043 + "node_type": "observation", 4044 + "title": "Current extension uses Manifest V3 with Chrome-specific APIs", 4045 + "description": null, 4046 + "status": "pending", 4047 + "created_at": "2025-12-28T18:10:08.441348100-05:00", 4048 + "updated_at": "2025-12-28T18:10:08.441348100-05:00", 4049 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4050 + }, 4051 + { 4052 + "id": 369, 4053 + "change_id": "783841d0-c096-48f6-be18-193a9dcc7d4b", 4054 + "node_type": "observation", 4055 + "title": "Firefox compatibility analysis: Extension uses chrome.* APIs (runtime.sendMessage, storage.local, tabs.query/sendMessage), MV3 service worker. Firefox supports MV3 but has differences. Options: 1) Use webextension-polyfill for cross-browser, 2) Dual manifests (MV3 Chrome + MV2 Firefox), 3) Keep MV3 for both with minimal changes. Current build outputs to dist/chrome only.", 4056 + "description": null, 4057 + "status": "pending", 4058 + "created_at": "2025-12-28T18:10:48.087066800-05:00", 4059 + "updated_at": "2025-12-28T18:10:48.087066800-05:00", 4060 + "metadata_json": "{\"branch\":\"master\",\"confidence\":90}" 4061 + }, 4062 + { 4063 + "id": 370, 4064 + "change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6", 4065 + "node_type": "decision", 4066 + "title": "Choose Firefox compatibility approach: webextension-polyfill, dual manifests, or minimal MV3 changes", 4067 + "description": null, 4068 + "status": "pending", 4069 + "created_at": "2025-12-28T18:10:50.375270400-05:00", 4070 + "updated_at": "2025-12-28T18:10:50.375270400-05:00", 4071 + "metadata_json": "{\"branch\":\"master\",\"confidence\":80}" 4072 + }, 4073 + { 4074 + "id": 371, 4075 + "change_id": "159906da-984f-4a1d-a1a6-98e0fc0cf369", 4076 + "node_type": "option", 4077 + "title": "Use webextension-polyfill library for unified cross-browser API", 4078 + "description": null, 4079 + "status": "pending", 4080 + "created_at": "2025-12-28T18:11:05.947924200-05:00", 4081 + "updated_at": "2025-12-28T18:11:05.947924200-05:00", 4082 + "metadata_json": "{\"branch\":\"master\",\"confidence\":85}" 4083 + }, 4084 + { 4085 + "id": 372, 4086 + "change_id": "df5e42e6-53c1-4b30-8b6f-f2385cd9e247", 4087 + "node_type": "option", 4088 + "title": "Dual manifests: MV3 for Chrome, MV2 for Firefox with separate builds", 4089 + "description": null, 4090 + "status": "pending", 4091 + "created_at": "2025-12-28T18:11:08.179938100-05:00", 4092 + "updated_at": "2025-12-28T18:11:08.179938100-05:00", 4093 + "metadata_json": "{\"branch\":\"master\",\"confidence\":85}" 4094 + }, 4095 + { 4096 + "id": 373, 4097 + "change_id": "7bb58202-7a9b-4e8b-8b9e-927e5106bce7", 4098 + "node_type": "option", 4099 + "title": "Keep MV3 for both browsers with minimal manifest tweaks", 4100 + "description": null, 4101 + "status": "pending", 4102 + "created_at": "2025-12-28T18:11:10.370113600-05:00", 4103 + "updated_at": "2025-12-28T18:11:10.370113600-05:00", 4104 + "metadata_json": "{\"branch\":\"master\",\"confidence\":85}" 4105 + }, 4106 + { 4107 + "id": 374, 4108 + "change_id": "d41b29e0-cd48-4dac-a6c8-c6179612702e", 4109 + "node_type": "outcome", 4110 + "title": "Chose webextension-polyfill approach. Provides unified browser.* API, Promise-based, future-proof MV3 for both browsers, +20KB but cleaner codebase", 4111 + "description": null, 4112 + "status": "pending", 4113 + "created_at": "2025-12-28T19:04:24.676770900-05:00", 4114 + "updated_at": "2025-12-28T19:04:24.676770900-05:00", 4115 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4116 + }, 4117 + { 4118 + "id": 375, 4119 + "change_id": "5bb34b8b-aec4-4f84-993e-eb9bf7a2d13f", 4120 + "node_type": "action", 4121 + "title": "Installing webextension-polyfill and updating source files to use browser.* API", 4122 + "description": null, 4123 + "status": "pending", 4124 + "created_at": "2025-12-28T19:08:14.642882400-05:00", 4125 + "updated_at": "2025-12-28T19:08:14.642882400-05:00", 4126 + "metadata_json": "{\"branch\":\"master\",\"confidence\":90}" 4127 + }, 4128 + { 4129 + "id": 376, 4130 + "change_id": "644181ee-5a44-4967-9657-e9dd5f648c5e", 4131 + "node_type": "outcome", 4132 + "title": "Successfully implemented Firefox compatibility with webextension-polyfill. Both Chrome and Firefox builds compile successfully. Chrome uses service_worker (MV3), Firefox uses scripts array with browser_specific_settings. All chrome.* API calls replaced with browser.* imports.", 4133 + "description": null, 4134 + "status": "pending", 4135 + "created_at": "2025-12-28T19:14:22.309457600-05:00", 4136 + "updated_at": "2025-12-28T19:14:22.309457600-05:00", 4137 + "metadata_json": "{\"branch\":\"master\",\"confidence\":95}" 4017 4138 } 4018 4139 ], 4019 4140 "edges": [ ··· 8009 8130 "weight": 1.0, 8010 8131 "rationale": "CORS fix completed", 8011 8132 "created_at": "2025-12-27T22:41:44.160528300-05:00" 8133 + }, 8134 + { 8135 + "id": 364, 8136 + "from_node_id": 360, 8137 + "to_node_id": 366, 8138 + "from_change_id": "706d5a7f-08ed-43f7-aee5-0bed28d9402a", 8139 + "to_change_id": "77b7ed7e-a113-41f6-a677-50d376f3f008", 8140 + "edge_type": "leads_to", 8141 + "weight": 1.0, 8142 + "rationale": "Git commit for CORS fixes", 8143 + "created_at": "2025-12-27T22:42:51.663598100-05:00" 8144 + }, 8145 + { 8146 + "id": 365, 8147 + "from_node_id": 367, 8148 + "to_node_id": 368, 8149 + "from_change_id": "df6abf7a-e7a4-45f3-8485-b933319416d9", 8150 + "to_change_id": "79721edf-aa05-4580-8c28-7d20941ef155", 8151 + "edge_type": "leads_to", 8152 + "weight": 1.0, 8153 + "rationale": "Analysis step for Firefox compatibility", 8154 + "created_at": "2025-12-28T18:10:09.484445500-05:00" 8155 + }, 8156 + { 8157 + "id": 366, 8158 + "from_node_id": 368, 8159 + "to_node_id": 369, 8160 + "from_change_id": "79721edf-aa05-4580-8c28-7d20941ef155", 8161 + "to_change_id": "783841d0-c096-48f6-be18-193a9dcc7d4b", 8162 + "edge_type": "leads_to", 8163 + "weight": 1.0, 8164 + "rationale": "Detailed analysis of compatibility issues", 8165 + "created_at": "2025-12-28T18:10:49.163552300-05:00" 8166 + }, 8167 + { 8168 + "id": 367, 8169 + "from_node_id": 369, 8170 + "to_node_id": 370, 8171 + "from_change_id": "783841d0-c096-48f6-be18-193a9dcc7d4b", 8172 + "to_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6", 8173 + "edge_type": "leads_to", 8174 + "weight": 1.0, 8175 + "rationale": "Need to decide implementation strategy", 8176 + "created_at": "2025-12-28T18:10:51.434960600-05:00" 8177 + }, 8178 + { 8179 + "id": 368, 8180 + "from_node_id": 370, 8181 + "to_node_id": 371, 8182 + "from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6", 8183 + "to_change_id": "159906da-984f-4a1d-a1a6-98e0fc0cf369", 8184 + "edge_type": "leads_to", 8185 + "weight": 1.0, 8186 + "rationale": "Option A", 8187 + "created_at": "2025-12-28T18:11:07.060637-05:00" 8188 + }, 8189 + { 8190 + "id": 369, 8191 + "from_node_id": 370, 8192 + "to_node_id": 372, 8193 + "from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6", 8194 + "to_change_id": "df5e42e6-53c1-4b30-8b6f-f2385cd9e247", 8195 + "edge_type": "leads_to", 8196 + "weight": 1.0, 8197 + "rationale": "Option B", 8198 + "created_at": "2025-12-28T18:11:09.223792400-05:00" 8199 + }, 8200 + { 8201 + "id": 370, 8202 + "from_node_id": 370, 8203 + "to_node_id": 373, 8204 + "from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6", 8205 + "to_change_id": "7bb58202-7a9b-4e8b-8b9e-927e5106bce7", 8206 + "edge_type": "leads_to", 8207 + "weight": 1.0, 8208 + "rationale": "Option C", 8209 + "created_at": "2025-12-28T18:11:11.439827800-05:00" 8210 + }, 8211 + { 8212 + "id": 371, 8213 + "from_node_id": 370, 8214 + "to_node_id": 374, 8215 + "from_change_id": "fd2d5b63-c26c-4592-89a6-3ccb4234c3c6", 8216 + "to_change_id": "d41b29e0-cd48-4dac-a6c8-c6179612702e", 8217 + "edge_type": "leads_to", 8218 + "weight": 1.0, 8219 + "rationale": "User selected option 1", 8220 + "created_at": "2025-12-28T19:04:26.708742600-05:00" 8221 + }, 8222 + { 8223 + "id": 372, 8224 + "from_node_id": 374, 8225 + "to_node_id": 375, 8226 + "from_change_id": "d41b29e0-cd48-4dac-a6c8-c6179612702e", 8227 + "to_change_id": "5bb34b8b-aec4-4f84-993e-eb9bf7a2d13f", 8228 + "edge_type": "leads_to", 8229 + "weight": 1.0, 8230 + "rationale": "Implementation based on decision", 8231 + "created_at": "2025-12-28T19:08:16.677078600-05:00" 8232 + }, 8233 + { 8234 + "id": 373, 8235 + "from_node_id": 375, 8236 + "to_node_id": 376, 8237 + "from_change_id": "5bb34b8b-aec4-4f84-993e-eb9bf7a2d13f", 8238 + "to_change_id": "644181ee-5a44-4967-9657-e9dd5f648c5e", 8239 + "edge_type": "leads_to", 8240 + "weight": 1.0, 8241 + "rationale": "Implementation completed successfully", 8242 + "created_at": "2025-12-28T19:14:24.961595600-05:00" 8012 8243 } 8013 8244 ] 8014 8245 }
+173
packages/extension/FIREFOX.md
··· 1 + # Firefox Extension Installation Guide 2 + 3 + The ATlast Importer extension now supports both Chrome and Firefox! 4 + 5 + ## Building for Firefox 6 + 7 + The build system automatically creates both Chrome and Firefox versions: 8 + 9 + ```bash 10 + pnpm run build # Development build for both browsers 11 + pnpm run build:prod # Production build for both browsers 12 + ``` 13 + 14 + Output directories: 15 + - `dist/chrome/` - Chrome/Edge version (Manifest V3 with service worker) 16 + - `dist/firefox/` - Firefox version (Manifest V3 with scripts array) 17 + 18 + ## Installing in Firefox (Development) 19 + 20 + ### Option 1: Temporary Installation (for testing) 21 + 22 + 1. Open Firefox 23 + 2. Navigate to `about:debugging#/runtime/this-firefox` 24 + 3. Click "Load Temporary Add-on..." 25 + 4. Navigate to `packages/extension/dist/firefox/` 26 + 5. Select the `manifest.json` file 27 + 28 + **Note:** Temporary extensions are removed when Firefox restarts. 29 + 30 + ### Option 2: Loading from ZIP (for distribution) 31 + 32 + 1. Build the production version: 33 + ```bash 34 + pnpm run build:prod 35 + pnpm run package:firefox 36 + ``` 37 + 38 + 2. This creates `dist/firefox.zip` 39 + 40 + 3. For testing: 41 + - Go to `about:debugging#/runtime/this-firefox` 42 + - Click "Load Temporary Add-on..." 43 + - Select the `firefox.zip` file 44 + 45 + 4. For publishing: 46 + - Submit `firefox.zip` to [addons.mozilla.org](https://addons.mozilla.org/developers/) 47 + 48 + ## Key Differences from Chrome Version 49 + 50 + ### Manifest Differences 51 + 52 + **Chrome (`manifest.chrome.json`):** 53 + ```json 54 + { 55 + "manifest_version": 3, 56 + "background": { 57 + "service_worker": "background/service-worker.js", 58 + "type": "module" 59 + } 60 + } 61 + ``` 62 + 63 + **Firefox (`manifest.firefox.json`):** 64 + ```json 65 + { 66 + "manifest_version": 3, 67 + "background": { 68 + "scripts": ["background/service-worker.js"], 69 + "type": "module" 70 + }, 71 + "browser_specific_settings": { 72 + "gecko": { 73 + "id": "atlast-importer@byarielm.fyi", 74 + "strict_min_version": "109.0" 75 + } 76 + } 77 + } 78 + ``` 79 + 80 + ### Cross-Browser Compatibility 81 + 82 + - All code uses `webextension-polyfill` library 83 + - Chrome-specific `chrome.*` APIs replaced with unified `browser.*` API 84 + - Promise-based instead of callback-based 85 + - Single codebase works across both browsers 86 + 87 + ### Requirements 88 + 89 + - **Firefox:** Version 109+ (for Manifest V3 support) 90 + - **Chrome/Edge:** Latest version 91 + 92 + ## Testing 93 + 94 + After loading the extension in Firefox: 95 + 96 + 1. Navigate to Twitter/X Following page (e.g., `https://twitter.com/username/following`) 97 + 2. Click the extension icon in the toolbar 98 + 3. The popup should show "Ready to scan" state 99 + 4. Click "Start Scan" to scrape usernames 100 + 5. Click "Open on ATlast" to upload results 101 + 102 + ## Debugging 103 + 104 + ### View Console Logs 105 + 106 + **Background Script:** 107 + - Go to `about:debugging#/runtime/this-firefox` 108 + - Find "ATlast Importer" in the list 109 + - Click "Inspect" 110 + 111 + **Popup:** 112 + - Right-click extension icon → "Inspect Extension" 113 + 114 + **Content Script:** 115 + - Open DevTools on Twitter/X page (F12) 116 + - Look for `[ATlast]` prefixed logs in Console 117 + 118 + ### Common Issues 119 + 120 + 1. **Extension not loading:** 121 + - Check Firefox version is 109+ 122 + - Ensure manifest.json is valid 123 + - Check browser console for errors 124 + 125 + 2. **Scan not starting:** 126 + - Verify you're on Twitter/X Following page 127 + - Check content script is injected (look for console logs) 128 + - Ensure page is fully loaded 129 + 130 + 3. **"Server offline" message:** 131 + - Make sure dev server is running (`netlify dev`) 132 + - Check API URL in extension settings 133 + 134 + ## Packaging for Distribution 135 + 136 + Create production builds for both browsers: 137 + 138 + ```bash 139 + pnpm run package:prod 140 + ``` 141 + 142 + This creates: 143 + - `dist/chrome.zip` - Ready for Chrome Web Store 144 + - `dist/firefox.zip` - Ready for Firefox Add-ons 145 + 146 + ## Development Workflow 147 + 148 + ```bash 149 + # Watch mode (auto-rebuild on changes) 150 + pnpm run dev 151 + 152 + # In Firefox: 153 + # 1. about:debugging → Reload extension after each rebuild 154 + # 2. Or use web-ext for auto-reload: 155 + 156 + npx web-ext run --source-dir=dist/firefox 157 + ``` 158 + 159 + ## Differences You Might Notice 160 + 161 + 1. **Background page persistence:** 162 + - Chrome: Service worker (non-persistent) 163 + - Firefox: Scripts array (similar behavior in MV3) 164 + 165 + 2. **API behavior:** 166 + - Firefox: Native Promise support 167 + - Chrome: Promises via polyfill 168 + 169 + 3. **Extension ID:** 170 + - Chrome: Auto-generated 171 + - Firefox: Explicitly set as `atlast-importer@byarielm.fyi` 172 + 173 + Both versions use the same source code and should behave identically!
+61 -42
packages/extension/build.js
··· 21 21 console.log(`🔗 API URL: ${ATLAST_API_URL}`); 22 22 23 23 // Clean dist directory 24 - const distDir = path.join(__dirname, 'dist', 'chrome'); 25 - if (fs.existsSync(distDir)) { 26 - fs.rmSync(distDir, { recursive: true }); 24 + const distBaseDir = path.join(__dirname, 'dist'); 25 + if (fs.existsSync(distBaseDir)) { 26 + fs.rmSync(distBaseDir, { recursive: true }); 27 27 } 28 - fs.mkdirSync(distDir, { recursive: true }); 28 + fs.mkdirSync(distBaseDir, { recursive: true }); 29 29 30 30 // Build configuration base 31 31 const buildConfigBase = { ··· 38 38 '__ATLAST_API_URL__': JSON.stringify(ATLAST_API_URL), 39 39 '__BUILD_MODE__': JSON.stringify(mode), 40 40 }, 41 + // Include webextension-polyfill in the bundle 42 + external: [], 41 43 }; 42 44 43 - // Build scripts 44 - const scripts = [ 45 - { 46 - ...buildConfigBase, 47 - entryPoints: ['src/content/index.ts'], 48 - outfile: path.join(distDir, 'content', 'index.js'), 49 - }, 50 - { 51 - ...buildConfigBase, 52 - entryPoints: ['src/background/service-worker.ts'], 53 - outfile: path.join(distDir, 'background', 'service-worker.js'), 54 - }, 55 - { 56 - ...buildConfigBase, 57 - entryPoints: ['src/popup/popup.ts'], 58 - outfile: path.join(distDir, 'popup', 'popup.js'), 59 - }, 60 - ]; 45 + // Build scripts for a specific browser 46 + function getScripts(browser) { 47 + const distDir = path.join(distBaseDir, browser); 48 + return [ 49 + { 50 + ...buildConfigBase, 51 + entryPoints: ['src/content/index.ts'], 52 + outfile: path.join(distDir, 'content', 'index.js'), 53 + }, 54 + { 55 + ...buildConfigBase, 56 + entryPoints: ['src/background/service-worker.ts'], 57 + outfile: path.join(distDir, 'background', 'service-worker.js'), 58 + }, 59 + { 60 + ...buildConfigBase, 61 + entryPoints: ['src/popup/popup.ts'], 62 + outfile: path.join(distDir, 'popup', 'popup.js'), 63 + }, 64 + ]; 65 + } 61 66 62 67 // Build function 63 68 async function build() { 64 69 try { 65 - console.log('🔨 Building extension...'); 70 + console.log('🔨 Building extension for Chrome and Firefox...'); 71 + 72 + const browsers = ['chrome', 'firefox']; 66 73 67 - // Build all scripts 68 - for (const config of scripts) { 69 - if (watch) { 70 - const ctx = await esbuild.context(config); 71 - await ctx.watch(); 72 - console.log(`👀 Watching ${path.basename(config.entryPoints[0])}...`); 73 - } else { 74 - await esbuild.build(config); 75 - console.log(`✅ Built ${path.basename(config.entryPoints[0])}`); 74 + for (const browser of browsers) { 75 + console.log(`\n📦 Building ${browser} version...`); 76 + const scripts = getScripts(browser); 77 + 78 + // Build all scripts 79 + for (const config of scripts) { 80 + if (watch) { 81 + const ctx = await esbuild.context(config); 82 + await ctx.watch(); 83 + console.log(`👀 Watching ${browser}/${path.basename(config.entryPoints[0])}...`); 84 + } else { 85 + await esbuild.build(config); 86 + console.log(`✅ Built ${browser}/${path.basename(config.entryPoints[0])}`); 87 + } 76 88 } 77 - } 78 89 79 - // Copy static files 80 - copyStaticFiles(); 90 + // Copy static files 91 + copyStaticFiles(browser); 81 92 82 - // Process CSS with Tailwind 83 - await processCSS(); 93 + // Process CSS with Tailwind 94 + await processCSS(browser); 95 + } 84 96 85 97 if (!watch) { 86 - console.log('✨ Build complete!'); 98 + console.log('\n✨ Build complete for both browsers!'); 87 99 } 88 100 } catch (error) { 89 101 console.error('❌ Build failed:', error); ··· 92 104 } 93 105 94 106 // Process CSS with PostCSS (Tailwind + Autoprefixer) 95 - async function processCSS() { 107 + async function processCSS(browser) { 96 108 const cssPath = path.join(__dirname, 'src/popup/popup.css'); 109 + const distDir = path.join(distBaseDir, browser); 97 110 const outputPath = path.join(distDir, 'popup/popup.css'); 98 111 99 112 const css = fs.readFileSync(cssPath, 'utf8'); ··· 121 134 } 122 135 123 136 // Copy static files 124 - function copyStaticFiles() { 137 + function copyStaticFiles(browser) { 138 + const distDir = path.join(distBaseDir, browser); 139 + 125 140 const filesToCopy = [ 126 - { from: 'manifest.json', to: 'manifest.json' }, 141 + { from: `manifest.${browser}.json`, to: 'manifest.json', fallback: 'manifest.json' }, 127 142 { from: 'src/popup/popup.html', to: 'popup/popup.html' }, 128 143 ]; 129 144 130 145 for (const file of filesToCopy) { 131 - const srcPath = path.join(__dirname, file.from); 146 + // Try to use browser-specific file first, fall back to default 147 + let srcPath = path.join(__dirname, file.from); 148 + if (file.fallback && !fs.existsSync(srcPath)) { 149 + srcPath = path.join(__dirname, file.fallback); 150 + } 132 151 const destPath = path.join(distDir, file.to); 133 152 134 153 // Create directory if it doesn't exist
+44
packages/extension/manifest.chrome.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 + }
+50
packages/extension/manifest.firefox.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 + "scripts": ["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 + "browser_specific_settings": { 45 + "gecko": { 46 + "id": "atlast-importer@byarielm.fyi", 47 + "strict_min_version": "109.0" 48 + } 49 + } 50 + }
+6 -2
packages/extension/package.json
··· 9 9 "build:prod": "node build.js --prod", 10 10 "dev": "node build.js --watch", 11 11 "package:chrome": "cd dist/chrome && zip -r ../chrome.zip .", 12 - "package:prod": "npm run build:prod && npm run package:chrome" 12 + "package:firefox": "cd dist/firefox && zip -r ../firefox.zip .", 13 + "package:all": "pnpm run package:chrome && pnpm run package:firefox", 14 + "package:prod": "pnpm run build:prod && pnpm run package:all" 13 15 }, 14 16 "dependencies": { 15 - "@atlast/shared": "workspace:*" 17 + "@atlast/shared": "workspace:*", 18 + "webextension-polyfill": "^0.12.0" 16 19 }, 17 20 "devDependencies": { 18 21 "@types/chrome": "^0.0.256", 22 + "@types/webextension-polyfill": "^0.12.4", 19 23 "autoprefixer": "^10.4.23", 20 24 "cssnano": "^7.1.2", 21 25 "esbuild": "^0.19.11",
+2 -1
packages/extension/src/background/service-worker.ts
··· 1 + import browser from 'webextension-polyfill'; 1 2 import { 2 3 MessageType, 3 4 onMessage, ··· 150 151 /** 151 152 * Log extension installation 152 153 */ 153 - chrome.runtime.onInstalled.addListener((details) => { 154 + browser.runtime.onInstalled.addListener((details) => { 154 155 console.log('[Background] Extension installed:', details.reason); 155 156 156 157 if (details.reason === 'install') {
+3 -1
packages/extension/src/lib/api-client.ts
··· 2 2 * ATlast API client for extension 3 3 */ 4 4 5 + import browser from 'webextension-polyfill'; 6 + 5 7 // These are replaced at build time by esbuild 6 8 declare const __ATLAST_API_URL__: string; 7 9 declare const __BUILD_MODE__: string; ··· 67 69 * Get extension version from manifest 68 70 */ 69 71 export function getExtensionVersion(): string { 70 - return chrome.runtime.getManifest().version; 72 + return browser.runtime.getManifest().version; 71 73 } 72 74 73 75 /**
+6 -5
packages/extension/src/lib/messaging.ts
··· 1 + import browser from 'webextension-polyfill'; 1 2 import type { ScraperProgress, ScraperResult } from '../content/scrapers/base-scraper.js'; 2 3 3 4 /** ··· 87 88 * Send message to background script 88 89 */ 89 90 export function sendToBackground<T = any>(message: Message): Promise<T> { 90 - return chrome.runtime.sendMessage(message); 91 + return browser.runtime.sendMessage(message); 91 92 } 92 93 93 94 /** 94 95 * Send message to active tab's content script 95 96 */ 96 97 export async function sendToContent(message: Message): Promise<any> { 97 - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); 98 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 98 99 if (!tab.id) { 99 100 throw new Error('No active tab found'); 100 101 } 101 - return chrome.tabs.sendMessage(tab.id, message); 102 + return browser.tabs.sendMessage(tab.id, message); 102 103 } 103 104 104 105 /** 105 106 * Listen for messages 106 107 */ 107 108 export function onMessage( 108 - handler: (message: Message, sender: chrome.runtime.MessageSender) => any | Promise<any> 109 + handler: (message: Message, sender: browser.Runtime.MessageSender) => any | Promise<any> 109 110 ): void { 110 - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 111 + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 111 112 const result = handler(message, sender); 112 113 113 114 // Handle async handlers
+4 -3
packages/extension/src/lib/storage.ts
··· 1 + import browser from 'webextension-polyfill'; 1 2 import type { ExtensionState } from './messaging.js'; 2 3 3 4 /** ··· 11 12 * Get extension state from storage 12 13 */ 13 14 export async function getState(): Promise<ExtensionState> { 14 - const result = await chrome.storage.local.get(STORAGE_KEYS.STATE); 15 + const result = await browser.storage.local.get(STORAGE_KEYS.STATE); 15 16 return result[STORAGE_KEYS.STATE] || { status: 'idle' }; 16 17 } 17 18 ··· 19 20 * Save extension state to storage 20 21 */ 21 22 export async function setState(state: ExtensionState): Promise<void> { 22 - await chrome.storage.local.set({ [STORAGE_KEYS.STATE]: state }); 23 + await browser.storage.local.set({ [STORAGE_KEYS.STATE]: state }); 23 24 } 24 25 25 26 /** 26 27 * Clear extension state 27 28 */ 28 29 export async function clearState(): Promise<void> { 29 - await chrome.storage.local.remove(STORAGE_KEYS.STATE); 30 + await browser.storage.local.remove(STORAGE_KEYS.STATE); 30 31 }
+4 -3
packages/extension/src/popup/popup.ts
··· 1 + import browser from 'webextension-polyfill'; 1 2 import { 2 3 MessageType, 3 4 sendToBackground, ··· 167 168 // Open ATlast at results page with upload data 168 169 const { getApiUrl } = await import('../lib/api-client.js'); 169 170 const resultsUrl = `${getApiUrl()}${response.redirectUrl}`; 170 - chrome.tabs.create({ url: resultsUrl }); 171 + browser.tabs.create({ url: resultsUrl }); 171 172 172 173 } catch (error) { 173 174 console.error('[Popup] Error uploading:', error); ··· 281 282 282 283 // Set up login buttons 283 284 elements.btnOpenAtlast.addEventListener('click', () => { 284 - chrome.tabs.create({ url: getApiUrl() }); 285 + browser.tabs.create({ url: getApiUrl() }); 285 286 }); 286 287 287 288 elements.btnRetryLogin.addEventListener('click', async () => { ··· 322 323 }); 323 324 324 325 // Listen for storage changes (when background updates state) 325 - chrome.storage.onChanged.addListener((changes, areaName) => { 326 + browser.storage.onChanged.addListener((changes, areaName) => { 326 327 if (areaName === 'local' && changes.extensionState) { 327 328 const newState = changes.extensionState.newValue; 328 329 console.log('[Popup] 🔄 Storage changed, new state:', newState);