Rules-based browser launcher for TUI + GNOME. switchyard.aly.codes
tui gome bowser go
0

Configure Feed

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

webextension: react rewrite, add caching

authored by

Aly Raffauf and committed by
Aly Raffauf
(May 24, 2026, 6:44 PM EDT) 273b75c5 5492f09d

+796 -66
+2
.gitignore
··· 10 10 build-dir/ 11 11 build-repo/ 12 12 build/ 13 + node_modules/ 14 + webextension/build/ 13 15 coverage.out 14 16 repo/ 15 17 switchyard
+8 -2
justfile
··· 67 67 [ -f build-repo/config ] || rm -rf build-repo 68 68 flatpak run org.flatpak.Builder --user --install --force-clean --repo=build-repo build-dir flatpak/{{APPID}}.Devel.yml 69 69 70 + # Build the browser extension (TypeScript + React) 71 + build-extension: 72 + cd webextension && npm ci && npm run build 73 + 70 74 # Bundle the browser extension into a zip archive 71 - bundle-extension: 75 + bundle-extension: build-extension 72 76 #!/usr/bin/env bash 73 77 set -euo pipefail 74 78 root="{{justfile_directory()}}" 75 79 extdir="${root}/webextension" 76 80 outfile="${root}/switchyard-webextension.zip" 77 81 rm -f "${outfile}" 78 - cd "${extdir}" && zip -r "${outfile}" . -x '.git*' 82 + cd "${extdir}" && zip -r "${outfile}" . \ 83 + -x '.git*' 'node_modules/*' 'src/*' \ 84 + -x 'package.json' 'package-lock.json' 'tsconfig.json' 'esbuild.config.mjs' 79 85 echo "Extension bundled: switchyard-webextension.zip" 80 86 81 87 # Cut a new release: bump version, commit, tag, and push master + tag
+20
webextension/esbuild.config.mjs
··· 1 + import * as esbuild from "esbuild"; 2 + 3 + const watch = process.argv.includes("--watch"); 4 + const ctx = await esbuild.context({ 5 + entryPoints: ["src/App.tsx"], 6 + bundle: true, 7 + outdir: "build", 8 + format: "esm", 9 + target: "es2022", 10 + sourcemap: watch ? "inline" : false, 11 + minify: !watch, 12 + }); 13 + 14 + if (watch) { 15 + await ctx.watch(); 16 + console.log("Watching..."); 17 + } else { 18 + await ctx.rebuild(); 19 + await ctx.dispose(); 20 + }
+610
webextension/package-lock.json
··· 1 + { 2 + "name": "switchyard-webextension", 3 + "version": "0.14.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "switchyard-webextension", 9 + "version": "0.14.0", 10 + "dependencies": { 11 + "react": "^19.1.0", 12 + "react-dom": "^19.1.0" 13 + }, 14 + "devDependencies": { 15 + "@types/chrome": "^0.1.30", 16 + "@types/react": "^19.1.2", 17 + "@types/react-dom": "^19.1.5", 18 + "esbuild": "^0.25.4", 19 + "typescript": "^5.8.3" 20 + } 21 + }, 22 + "node_modules/@esbuild/aix-ppc64": { 23 + "version": "0.25.12", 24 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", 25 + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", 26 + "cpu": [ 27 + "ppc64" 28 + ], 29 + "dev": true, 30 + "license": "MIT", 31 + "optional": true, 32 + "os": [ 33 + "aix" 34 + ], 35 + "engines": { 36 + "node": ">=18" 37 + } 38 + }, 39 + "node_modules/@esbuild/android-arm": { 40 + "version": "0.25.12", 41 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", 42 + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", 43 + "cpu": [ 44 + "arm" 45 + ], 46 + "dev": true, 47 + "license": "MIT", 48 + "optional": true, 49 + "os": [ 50 + "android" 51 + ], 52 + "engines": { 53 + "node": ">=18" 54 + } 55 + }, 56 + "node_modules/@esbuild/android-arm64": { 57 + "version": "0.25.12", 58 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", 59 + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", 60 + "cpu": [ 61 + "arm64" 62 + ], 63 + "dev": true, 64 + "license": "MIT", 65 + "optional": true, 66 + "os": [ 67 + "android" 68 + ], 69 + "engines": { 70 + "node": ">=18" 71 + } 72 + }, 73 + "node_modules/@esbuild/android-x64": { 74 + "version": "0.25.12", 75 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", 76 + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", 77 + "cpu": [ 78 + "x64" 79 + ], 80 + "dev": true, 81 + "license": "MIT", 82 + "optional": true, 83 + "os": [ 84 + "android" 85 + ], 86 + "engines": { 87 + "node": ">=18" 88 + } 89 + }, 90 + "node_modules/@esbuild/darwin-arm64": { 91 + "version": "0.25.12", 92 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", 93 + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", 94 + "cpu": [ 95 + "arm64" 96 + ], 97 + "dev": true, 98 + "license": "MIT", 99 + "optional": true, 100 + "os": [ 101 + "darwin" 102 + ], 103 + "engines": { 104 + "node": ">=18" 105 + } 106 + }, 107 + "node_modules/@esbuild/darwin-x64": { 108 + "version": "0.25.12", 109 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", 110 + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", 111 + "cpu": [ 112 + "x64" 113 + ], 114 + "dev": true, 115 + "license": "MIT", 116 + "optional": true, 117 + "os": [ 118 + "darwin" 119 + ], 120 + "engines": { 121 + "node": ">=18" 122 + } 123 + }, 124 + "node_modules/@esbuild/freebsd-arm64": { 125 + "version": "0.25.12", 126 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", 127 + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", 128 + "cpu": [ 129 + "arm64" 130 + ], 131 + "dev": true, 132 + "license": "MIT", 133 + "optional": true, 134 + "os": [ 135 + "freebsd" 136 + ], 137 + "engines": { 138 + "node": ">=18" 139 + } 140 + }, 141 + "node_modules/@esbuild/freebsd-x64": { 142 + "version": "0.25.12", 143 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", 144 + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", 145 + "cpu": [ 146 + "x64" 147 + ], 148 + "dev": true, 149 + "license": "MIT", 150 + "optional": true, 151 + "os": [ 152 + "freebsd" 153 + ], 154 + "engines": { 155 + "node": ">=18" 156 + } 157 + }, 158 + "node_modules/@esbuild/linux-arm": { 159 + "version": "0.25.12", 160 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", 161 + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", 162 + "cpu": [ 163 + "arm" 164 + ], 165 + "dev": true, 166 + "license": "MIT", 167 + "optional": true, 168 + "os": [ 169 + "linux" 170 + ], 171 + "engines": { 172 + "node": ">=18" 173 + } 174 + }, 175 + "node_modules/@esbuild/linux-arm64": { 176 + "version": "0.25.12", 177 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", 178 + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", 179 + "cpu": [ 180 + "arm64" 181 + ], 182 + "dev": true, 183 + "license": "MIT", 184 + "optional": true, 185 + "os": [ 186 + "linux" 187 + ], 188 + "engines": { 189 + "node": ">=18" 190 + } 191 + }, 192 + "node_modules/@esbuild/linux-ia32": { 193 + "version": "0.25.12", 194 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", 195 + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", 196 + "cpu": [ 197 + "ia32" 198 + ], 199 + "dev": true, 200 + "license": "MIT", 201 + "optional": true, 202 + "os": [ 203 + "linux" 204 + ], 205 + "engines": { 206 + "node": ">=18" 207 + } 208 + }, 209 + "node_modules/@esbuild/linux-loong64": { 210 + "version": "0.25.12", 211 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", 212 + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", 213 + "cpu": [ 214 + "loong64" 215 + ], 216 + "dev": true, 217 + "license": "MIT", 218 + "optional": true, 219 + "os": [ 220 + "linux" 221 + ], 222 + "engines": { 223 + "node": ">=18" 224 + } 225 + }, 226 + "node_modules/@esbuild/linux-mips64el": { 227 + "version": "0.25.12", 228 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", 229 + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", 230 + "cpu": [ 231 + "mips64el" 232 + ], 233 + "dev": true, 234 + "license": "MIT", 235 + "optional": true, 236 + "os": [ 237 + "linux" 238 + ], 239 + "engines": { 240 + "node": ">=18" 241 + } 242 + }, 243 + "node_modules/@esbuild/linux-ppc64": { 244 + "version": "0.25.12", 245 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", 246 + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", 247 + "cpu": [ 248 + "ppc64" 249 + ], 250 + "dev": true, 251 + "license": "MIT", 252 + "optional": true, 253 + "os": [ 254 + "linux" 255 + ], 256 + "engines": { 257 + "node": ">=18" 258 + } 259 + }, 260 + "node_modules/@esbuild/linux-riscv64": { 261 + "version": "0.25.12", 262 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", 263 + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", 264 + "cpu": [ 265 + "riscv64" 266 + ], 267 + "dev": true, 268 + "license": "MIT", 269 + "optional": true, 270 + "os": [ 271 + "linux" 272 + ], 273 + "engines": { 274 + "node": ">=18" 275 + } 276 + }, 277 + "node_modules/@esbuild/linux-s390x": { 278 + "version": "0.25.12", 279 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", 280 + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", 281 + "cpu": [ 282 + "s390x" 283 + ], 284 + "dev": true, 285 + "license": "MIT", 286 + "optional": true, 287 + "os": [ 288 + "linux" 289 + ], 290 + "engines": { 291 + "node": ">=18" 292 + } 293 + }, 294 + "node_modules/@esbuild/linux-x64": { 295 + "version": "0.25.12", 296 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", 297 + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", 298 + "cpu": [ 299 + "x64" 300 + ], 301 + "dev": true, 302 + "license": "MIT", 303 + "optional": true, 304 + "os": [ 305 + "linux" 306 + ], 307 + "engines": { 308 + "node": ">=18" 309 + } 310 + }, 311 + "node_modules/@esbuild/netbsd-arm64": { 312 + "version": "0.25.12", 313 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", 314 + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", 315 + "cpu": [ 316 + "arm64" 317 + ], 318 + "dev": true, 319 + "license": "MIT", 320 + "optional": true, 321 + "os": [ 322 + "netbsd" 323 + ], 324 + "engines": { 325 + "node": ">=18" 326 + } 327 + }, 328 + "node_modules/@esbuild/netbsd-x64": { 329 + "version": "0.25.12", 330 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", 331 + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", 332 + "cpu": [ 333 + "x64" 334 + ], 335 + "dev": true, 336 + "license": "MIT", 337 + "optional": true, 338 + "os": [ 339 + "netbsd" 340 + ], 341 + "engines": { 342 + "node": ">=18" 343 + } 344 + }, 345 + "node_modules/@esbuild/openbsd-arm64": { 346 + "version": "0.25.12", 347 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", 348 + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", 349 + "cpu": [ 350 + "arm64" 351 + ], 352 + "dev": true, 353 + "license": "MIT", 354 + "optional": true, 355 + "os": [ 356 + "openbsd" 357 + ], 358 + "engines": { 359 + "node": ">=18" 360 + } 361 + }, 362 + "node_modules/@esbuild/openbsd-x64": { 363 + "version": "0.25.12", 364 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", 365 + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", 366 + "cpu": [ 367 + "x64" 368 + ], 369 + "dev": true, 370 + "license": "MIT", 371 + "optional": true, 372 + "os": [ 373 + "openbsd" 374 + ], 375 + "engines": { 376 + "node": ">=18" 377 + } 378 + }, 379 + "node_modules/@esbuild/openharmony-arm64": { 380 + "version": "0.25.12", 381 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", 382 + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", 383 + "cpu": [ 384 + "arm64" 385 + ], 386 + "dev": true, 387 + "license": "MIT", 388 + "optional": true, 389 + "os": [ 390 + "openharmony" 391 + ], 392 + "engines": { 393 + "node": ">=18" 394 + } 395 + }, 396 + "node_modules/@esbuild/sunos-x64": { 397 + "version": "0.25.12", 398 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", 399 + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", 400 + "cpu": [ 401 + "x64" 402 + ], 403 + "dev": true, 404 + "license": "MIT", 405 + "optional": true, 406 + "os": [ 407 + "sunos" 408 + ], 409 + "engines": { 410 + "node": ">=18" 411 + } 412 + }, 413 + "node_modules/@esbuild/win32-arm64": { 414 + "version": "0.25.12", 415 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", 416 + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", 417 + "cpu": [ 418 + "arm64" 419 + ], 420 + "dev": true, 421 + "license": "MIT", 422 + "optional": true, 423 + "os": [ 424 + "win32" 425 + ], 426 + "engines": { 427 + "node": ">=18" 428 + } 429 + }, 430 + "node_modules/@esbuild/win32-ia32": { 431 + "version": "0.25.12", 432 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", 433 + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", 434 + "cpu": [ 435 + "ia32" 436 + ], 437 + "dev": true, 438 + "license": "MIT", 439 + "optional": true, 440 + "os": [ 441 + "win32" 442 + ], 443 + "engines": { 444 + "node": ">=18" 445 + } 446 + }, 447 + "node_modules/@esbuild/win32-x64": { 448 + "version": "0.25.12", 449 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", 450 + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", 451 + "cpu": [ 452 + "x64" 453 + ], 454 + "dev": true, 455 + "license": "MIT", 456 + "optional": true, 457 + "os": [ 458 + "win32" 459 + ], 460 + "engines": { 461 + "node": ">=18" 462 + } 463 + }, 464 + "node_modules/@types/chrome": { 465 + "version": "0.1.42", 466 + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.42.tgz", 467 + "integrity": "sha512-tdT2roFqGecZZDjA9fUEAINb2STxSPifHMDvY6EfRjNRCjdrs/0FwKt5RCIA9MKMd1arAYZZL3nwEkp6ZLZu2w==", 468 + "dev": true, 469 + "license": "MIT", 470 + "dependencies": { 471 + "@types/filesystem": "*", 472 + "@types/har-format": "*" 473 + } 474 + }, 475 + "node_modules/@types/filesystem": { 476 + "version": "0.0.36", 477 + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", 478 + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", 479 + "dev": true, 480 + "license": "MIT", 481 + "dependencies": { 482 + "@types/filewriter": "*" 483 + } 484 + }, 485 + "node_modules/@types/filewriter": { 486 + "version": "0.0.33", 487 + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", 488 + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", 489 + "dev": true, 490 + "license": "MIT" 491 + }, 492 + "node_modules/@types/har-format": { 493 + "version": "1.2.16", 494 + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", 495 + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", 496 + "dev": true, 497 + "license": "MIT" 498 + }, 499 + "node_modules/@types/react": { 500 + "version": "19.2.15", 501 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", 502 + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", 503 + "dev": true, 504 + "license": "MIT", 505 + "dependencies": { 506 + "csstype": "^3.2.2" 507 + } 508 + }, 509 + "node_modules/@types/react-dom": { 510 + "version": "19.2.3", 511 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", 512 + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", 513 + "dev": true, 514 + "license": "MIT", 515 + "peerDependencies": { 516 + "@types/react": "^19.2.0" 517 + } 518 + }, 519 + "node_modules/csstype": { 520 + "version": "3.2.3", 521 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", 522 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", 523 + "dev": true, 524 + "license": "MIT" 525 + }, 526 + "node_modules/esbuild": { 527 + "version": "0.25.12", 528 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", 529 + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", 530 + "dev": true, 531 + "hasInstallScript": true, 532 + "license": "MIT", 533 + "bin": { 534 + "esbuild": "bin/esbuild" 535 + }, 536 + "engines": { 537 + "node": ">=18" 538 + }, 539 + "optionalDependencies": { 540 + "@esbuild/aix-ppc64": "0.25.12", 541 + "@esbuild/android-arm": "0.25.12", 542 + "@esbuild/android-arm64": "0.25.12", 543 + "@esbuild/android-x64": "0.25.12", 544 + "@esbuild/darwin-arm64": "0.25.12", 545 + "@esbuild/darwin-x64": "0.25.12", 546 + "@esbuild/freebsd-arm64": "0.25.12", 547 + "@esbuild/freebsd-x64": "0.25.12", 548 + "@esbuild/linux-arm": "0.25.12", 549 + "@esbuild/linux-arm64": "0.25.12", 550 + "@esbuild/linux-ia32": "0.25.12", 551 + "@esbuild/linux-loong64": "0.25.12", 552 + "@esbuild/linux-mips64el": "0.25.12", 553 + "@esbuild/linux-ppc64": "0.25.12", 554 + "@esbuild/linux-riscv64": "0.25.12", 555 + "@esbuild/linux-s390x": "0.25.12", 556 + "@esbuild/linux-x64": "0.25.12", 557 + "@esbuild/netbsd-arm64": "0.25.12", 558 + "@esbuild/netbsd-x64": "0.25.12", 559 + "@esbuild/openbsd-arm64": "0.25.12", 560 + "@esbuild/openbsd-x64": "0.25.12", 561 + "@esbuild/openharmony-arm64": "0.25.12", 562 + "@esbuild/sunos-x64": "0.25.12", 563 + "@esbuild/win32-arm64": "0.25.12", 564 + "@esbuild/win32-ia32": "0.25.12", 565 + "@esbuild/win32-x64": "0.25.12" 566 + } 567 + }, 568 + "node_modules/react": { 569 + "version": "19.2.6", 570 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", 571 + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", 572 + "license": "MIT", 573 + "engines": { 574 + "node": ">=0.10.0" 575 + } 576 + }, 577 + "node_modules/react-dom": { 578 + "version": "19.2.6", 579 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", 580 + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", 581 + "license": "MIT", 582 + "dependencies": { 583 + "scheduler": "^0.27.0" 584 + }, 585 + "peerDependencies": { 586 + "react": "^19.2.6" 587 + } 588 + }, 589 + "node_modules/scheduler": { 590 + "version": "0.27.0", 591 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", 592 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", 593 + "license": "MIT" 594 + }, 595 + "node_modules/typescript": { 596 + "version": "5.9.3", 597 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 598 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 599 + "dev": true, 600 + "license": "Apache-2.0", 601 + "bin": { 602 + "tsc": "bin/tsc", 603 + "tsserver": "bin/tsserver" 604 + }, 605 + "engines": { 606 + "node": ">=14.17" 607 + } 608 + } 609 + } 610 + }
+21
webextension/package.json
··· 1 + { 2 + "name": "switchyard-webextension", 3 + "version": "0.14.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "build": "node esbuild.config.mjs", 8 + "dev": "node esbuild.config.mjs --watch" 9 + }, 10 + "dependencies": { 11 + "react": "^19.1.0", 12 + "react-dom": "^19.1.0" 13 + }, 14 + "devDependencies": { 15 + "@types/chrome": "^0.1.30", 16 + "@types/react": "^19.1.2", 17 + "@types/react-dom": "^19.1.5", 18 + "esbuild": "^0.25.4", 19 + "typescript": "^5.8.3" 20 + } 21 + }
+11 -10
webextension/popup.html
··· 11 11 --sep: rgba(0, 0, 0, 0.1); 12 12 --accent: #3584e4; 13 13 } 14 - :root.dark { 15 - --bg: #242424; 16 - --fg: #ececec; 17 - --muted: #a0a0a0; 18 - --hover: rgba(255, 255, 255, 0.1); 19 - --active: rgba(255, 255, 255, 0.18); 20 - --sep: rgba(255, 255, 255, 0.12); 21 - --accent: #62a0ea; 14 + @media (prefers-color-scheme: dark) { 15 + :root { 16 + --bg: #242424; 17 + --fg: #ececec; 18 + --muted: #a0a0a0; 19 + --hover: rgba(255, 255, 255, 0.1); 20 + --active: rgba(255, 255, 255, 0.18); 21 + --sep: rgba(255, 255, 255, 0.12); 22 + --accent: #62a0ea; 23 + } 22 24 } 23 25 body { 24 26 margin: 0; ··· 77 79 text-align: center; 78 80 } 79 81 </style> 80 - <div id="header" class="header" hidden></div> 81 82 <div id="root"></div> 82 - <script src="popup.js"></script> 83 + <script src="build/App.js"></script>
-54
webextension/popup.js
··· 1 - const root = document.getElementById("root"); 2 - const header = document.getElementById("header"); 3 - const HOST = "io.github.alyraffauf.switchyard"; 4 - const ROUTABLE = /^(https?|ftp|file):/; 5 - 6 - const dark = matchMedia("(prefers-color-scheme: dark)"); 7 - const setTheme = () => 8 - document.documentElement.classList.toggle("dark", dark.matches); 9 - dark.addEventListener("change", setTheme); 10 - setTheme(); 11 - 12 - const make = (tag, className, text) => { 13 - const el = document.createElement(tag); 14 - if (className) el.className = className; 15 - if (text) el.textContent = text; 16 - root.appendChild(el); 17 - return el; 18 - }; 19 - 20 - const launch = (tab, browser) => { 21 - const params = new URLSearchParams({ url: tab.url }); 22 - if (browser) params.set("browser", browser); 23 - chrome.tabs.update(tab.id, { url: `switchyard://open?${params}` }); 24 - window.close(); 25 - }; 26 - 27 - chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => { 28 - if (!tab?.url) return make("div", "hint", "No active tab"); 29 - if (!ROUTABLE.test(tab.url)) 30 - return make("div", "hint", "This page can't be opened in another browser"); 31 - 32 - // header.textContent = tab.title || tab.url; 33 - // header.title = tab.url; 34 - // header.hidden = false; 35 - 36 - make("button", "row primary", "Open in Switchyard").onclick = () => 37 - launch(tab); 38 - 39 - chrome.runtime.sendNativeMessage(HOST, { action: "listBrowsers" }, (resp) => { 40 - if (chrome.runtime.lastError) { 41 - make("div", "separator"); 42 - make("div", "hint", "Install the native host to list browsers"); 43 - return; 44 - } 45 - const browsers = resp?.browsers ?? []; 46 - if (!browsers.length) return; 47 - make("div", "separator"); 48 - for (const b of browsers) { 49 - const btn = make("button", "row", b.name); 50 - btn.title = b.id; 51 - btn.onclick = () => launch(tab, b.id); 52 - } 53 - }); 54 - });
+109
webextension/src/App.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { createRoot } from "react-dom/client"; 3 + 4 + interface Browser { 5 + id: string; 6 + name: string; 7 + } 8 + 9 + type BrowserList = { browsers: Browser[] }; 10 + 11 + const HOST = "io.github.alyraffauf.switchyard"; 12 + const ROUTABLE = /^(https?|ftp|file):/; 13 + const CACHE_KEY = "browsers_v2"; 14 + 15 + function getCachedBrowsers(): Browser[] { 16 + try { 17 + const raw = localStorage.getItem(CACHE_KEY); 18 + if (!raw) return []; 19 + const data = JSON.parse(raw); 20 + return Array.isArray(data) ? data : []; 21 + } catch { 22 + return []; 23 + } 24 + } 25 + 26 + function cacheBrowsers(browsers: Browser[]) { 27 + try { 28 + localStorage.setItem(CACHE_KEY, JSON.stringify(browsers)); 29 + } catch { 30 + /* quota exceeded — ignore */ 31 + } 32 + } 33 + 34 + function launch(url: string, tabId: number, browser?: string) { 35 + const params = new URLSearchParams({ url }); 36 + if (browser) params.set("browser", browser); 37 + chrome.tabs.update(tabId, { url: `switchyard://open?${params}` }); 38 + window.close(); 39 + } 40 + 41 + function App() { 42 + const [tab, setTab] = useState<chrome.tabs.Tab | null>(null); 43 + const [browsers, setBrowsers] = useState<Browser[]>(getCachedBrowsers); 44 + const [nativeHostMissing, setNativeHostMissing] = useState(false); 45 + 46 + useEffect(() => { 47 + chrome.tabs.query({ active: true, currentWindow: true }, ([t]) => 48 + setTab(t ?? null), 49 + ); 50 + }, []); 51 + 52 + useEffect(() => { 53 + if (!tab?.url || !ROUTABLE.test(tab.url)) return; 54 + 55 + chrome.runtime.sendNativeMessage( 56 + HOST, 57 + { action: "listBrowsers" }, 58 + (resp) => { 59 + if (chrome.runtime.lastError) { 60 + console.error( 61 + "Switchyard native host error:", 62 + chrome.runtime.lastError.message, 63 + ); 64 + setNativeHostMissing(true); 65 + return; 66 + } 67 + setNativeHostMissing(false); 68 + const list = (resp as BrowserList)?.browsers ?? []; 69 + cacheBrowsers(list); 70 + setBrowsers(list); 71 + }, 72 + ); 73 + }, [tab]); 74 + 75 + if (!tab?.url) return <div className="hint">No active tab</div>; 76 + if (!ROUTABLE.test(tab.url)) 77 + return ( 78 + <div className="hint">This page can't be opened in another browser</div> 79 + ); 80 + 81 + const { url } = tab; 82 + const tabId = tab.id!; 83 + 84 + return ( 85 + <> 86 + <button className="row primary" onClick={() => launch(url, tabId)}> 87 + Open in Switchyard 88 + </button> 89 + {(nativeHostMissing || browsers.length > 0) && ( 90 + <div className="separator" /> 91 + )} 92 + {nativeHostMissing && ( 93 + <div className="hint">Install the native host to list browsers</div> 94 + )} 95 + {browsers.map((b) => ( 96 + <button 97 + key={b.id} 98 + className="row" 99 + title={b.id} 100 + onClick={() => launch(url, tabId, b.id)} 101 + > 102 + {b.name} 103 + </button> 104 + ))} 105 + </> 106 + ); 107 + } 108 + 109 + createRoot(document.getElementById("root")!).render(<App />);
+15
webextension/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ES2022", 5 + "moduleResolution": "bundler", 6 + "jsx": "react-jsx", 7 + "strict": true, 8 + "esModuleInterop": true, 9 + "skipLibCheck": true, 10 + "forceConsistentCasingInFileNames": true, 11 + "outDir": "build", 12 + "rootDir": "src" 13 + }, 14 + "include": ["src"] 15 + }