diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1555507..ba2710a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,23 +14,16 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: '22'
cache: npm
- # The build downscales the toolbar icons with sharp, so the dev
- # dependencies have to be there before anything below runs.
- name: Install dependencies
run: npm ci
- # Matches the signature rules against the real bundle output committed
- # under test/fixtures.
- name: Signature tests
run: npm test
- name: Build both browser targets
run: npm run build
- # Catches a manifest that points at a file the build did not copy, or the
- # two manifests drifting apart.
- name: Validate build output
run: npm run validate
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..73f748b
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,56 @@
+name: Release Extension
+
+on:
+ workflow_dispatch:
+
+jobs:
+ publish_firefox:
+ environment: firefox-release
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Publish to Firefox Add-ons
+ run: |
+ npx web-ext@latest sign \
+ --source-dir ./dist/firefox \
+ --channel listed \
+ --api-key "${{ secrets.AMO_JWT_ISSUER }}" \
+ --api-secret "${{ secrets.AMO_JWT_SECRET }}"
+
+ publish_chrome:
+ environment: chrome-release
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Publish to Chrome Web Store
+ uses: mnao305/chrome-extension-upload@v6.0.0
+ with:
+ file-path: dist/chrome.zip
+ extension-id: jagakebomapmaajkbbldhbilhgedlkmp
+ client-id: ${{ secrets.CLIENT_ID }}
+ client-secret: ${{ secrets.CLIENT_SECRET }}
+ refresh-token: ${{ secrets.REFRESH_TOKEN }}
diff --git a/README.md b/README.md
index 1e582fa..91e18cc 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,69 @@ A Firefox and Chrome extension that works out what the page you are looking at
was built with — the framework, the UI library and the bundler — and turns the
toolbar icon into that project's logo.
-- A Next.js site → the Next.js mark.
-- A Vue SPA on Vite → the Vue mark, with Vite in the popup.
-- Built with Rspack and nothing else recognisable → the Rspack crab.
-- Two rival frameworks on one page → 😈, and the popup lists everything.
+Available on the official stores:
-Click the icon for the details: what was found, which version (as precisely as
-the page allows), what evidence was absorbed, and the exact byte patterns that
-led to that conclusion.
+- [Chrome Web Store](https://chromewebstore.google.com/detail/web-stack-detector/jagakebomapmaajkbbldhbilhgedlkmp)
+- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/web-stack-detector/)
-
+
+Screenshots
-Not on any store — clone it and load it yourself.
+
+
+
+
+## Features
+
+Currently supports the following technologies.
+
+### Meta-framework
+
+| Name | Version range |
+| --- | --- |
+| Next.js | Exact version |
+| Fresh | ≤ 1 or ≥ 2 |
+| Nuxt | — |
+| SvelteKit | — |
+| Astro | Exact version |
+| Gatsby | Exact version |
+| Docusaurus | Exact version |
+| VitePress | Exact version |
+| Rspress | Exact version |
+| Remix | Exact version (3.x) |
+
+### UI library
+
+| Name | Version range |
+| --- | --- |
+| React | Major version |
+| Preact | — |
+| Vue | 2 / 3 |
+| Angular | Exact version |
+| AngularJS | Exact version |
+| Svelte | Major version |
+| Solid | — |
+| Qwik | Exact version |
+| Lit | Exact version |
+| Alpine.js | Exact version |
+| htmx | Exact version |
+| jQuery | Exact version |
+| Stimulus | — |
+| Ember.js | — |
+| Backbone.js | Exact version |
+| Knockout | Exact version |
+
+### Bundler
+
+| Name | Version range |
+| --- | --- |
+| Vite | Major version range |
+| webpack | Major version |
+| Rspack | 1.x exact / ≥ 2 |
+| Turbopack | — |
+| Parcel | Major version |
+| esbuild | — |
+| Rollup | — |
## Build
@@ -41,279 +89,6 @@ dist/firefox manifest with an event-page background + a gecko id
They share every line of code and differ only in the manifest.
-## Install in Chrome
-
-1. Open `chrome://extensions`
-2. Turn on **Developer mode** (top right)
-3. Click **Load unpacked** and pick the `dist/chrome` folder
-
-The icon appears straight away and survives restarts. Chrome shows a
-*"Disable developer mode extensions"* warning on startup — that is the normal
-nag for any unpacked extension.
-
-Click the puzzle-piece icon and pin *Web Stack Detector* so the logo stays visible
-in the toolbar, which is rather the point of it.
-
-After changing the source, run `npm run build` again and press **reload** ↻ on
-the extension's card.
-
-## Install in Firefox
-
-1. Open `about:debugging#/runtime/this-firefox`
-2. Click **Load Temporary Add-on…**
-3. Pick `dist/firefox/manifest.json` (the file, not the folder)
-
-Then **grant site access**, or it will detect much less: Firefox MV3 makes host
-permissions opt-in, so until you allow it the extension can read the page but
-never the script files themselves.
-
-- Click the puzzle-piece icon → the gear next to *Web Stack Detector* → **Always
- Allow on All Sites**
-- Or `about:addons` → Web Stack Detector → **Permissions** → *Access your data for
- all websites*
-
-**Temporary add-ons disappear when Firefox restarts.** Loading it again is the
-same three steps. A permanent install would need the add-on signed by Mozilla.
-
-## Check it works
-
-Visit a few sites and watch the icon:
-
-| Site | Expected |
-| --- | --- |
-| | Next.js, absorbing React + Turbopack traces |
-| | VitePress, absorbing Vue + Vite traces |
-| | Angular 22.1.3 |
-| | SvelteKit, absorbing Svelte 5 + Vite traces |
-| | jQuery 4.0.0 |
-| | Svelte 5, with jQuery, Stimulus and webpack |
-
-Those are the verdicts it actually produced when run against the live sites.
-
-## What it detects
-
-Three categories, in the order they claim the icon: a **framework** says more
-about a page than the **UI library** underneath it, which says more than the
-**bundler** underneath that.
-
-### Frameworks
-
-| | Recognised by | Version |
-| --- | --- | --- |
-| **Next.js** | `window.next`, `__NEXT_DATA__`, `self.__next_f`, `/_next/static/`, the `webpackChunk_N_E` registry | **exact**, off `window.next.version` |
-| **Fresh** | Fresh 1's `/_frsh/` runtime assets and `__FRSH_STATE`; Fresh 2's `f-client-nav` / `f-partial` attributes | `≤ 1` or `≥ 2`, from the runtime marker |
-| **Nuxt** | `useNuxtApp`, `__NUXT__`, `__buildAssetsURL`, `/_nuxt/` | none |
-| **SvelteKit** | `__sveltekit_…`, `/_app/immutable/`, `data-sveltekit-preload-data` | none |
-| **Astro** | `` and its runtime, `astro:page-load`, `/_astro/`, `data-astro-cid-…` | **exact** from the generator meta tag |
-| **Gatsby** | `___loader`, `___chunkMapping`, `id="___gatsby"`, `/page-data/` | **exact** from the generator meta tag |
-| **Docusaurus** | `window.docusaurus`, `id="__docusaurus"` | **exact** from the generator meta tag |
-| **VitePress** | `__VITEPRESS__`, `__VP_HASH_MAP__` | **exact** from the generator meta tag |
-| **Rspress** | `id="__rspress_root"` (2.x), the `rspress-doc`/`rspress-nav` theme classes, the `rspress-theme-appearance` script | **exact** from the generator meta tag |
-| **Remix** | `__remixContext`; Remix 3's `rmx-…`/`data-remix-…` attributes, `rmxc-…` classes and `@remix-run/ui` requests | **exact** for Remix 3, off the asset path |
-
-### UI libraries
-
-| | Recognised by | Version |
-| --- | --- | --- |
-| **React** | `__reactFiber$…` / `__reactProps$…` on DOM nodes, `_reactListening…`, the minified error-decoder URL | major only, from the decoder URL moving to `react.dev` in 19 |
-| **Preact** | the mangled `__k`/`__c`/`__e`/`__b` vnode internals, `__k` on DOM nodes, `window.preact` | none |
-| **Vue** | `__vue_app__` and `_vnode` on DOM nodes, `__VUE__`, `__v_isVNode`, `__vccOpts`, `data-v-…` | 2 vs 3; exact from a dev-build banner |
-| **Angular** | `ng-version` on the root element, `__ngContext__`, `_nghost-`/`_ngcontent-`, `ɵcmp` | **exact**, off `ng-version` |
-| **AngularJS** | `window.angular`, `ng-app`/`ng-controller`, the `ng-scope` class | **exact**, off `angular.version.full` |
-| **Svelte** | `window.__svelte`, `svelte.dev/e/…` error URLs, `svelte-…` scoped classes | **major**, off `__svelte.v` |
-| **Solid** | `_$DX_DELEGATE`, `$$click`-style handlers on DOM nodes | none |
-| **Qwik** | `q:container` / `q:version` attributes, `_qc_`, `window.qwikevents` | **exact**, off `q:version` |
-| **Lit** | `litElementVersions` on window, `$lit$` template markers | **exact**, off `litElementVersions` |
-| **Alpine.js** | `window.Alpine`, `_x_…` on DOM nodes, `x-data`/`x-show` | **exact**, off `Alpine.version` |
-| **htmx** | `window.htmx`, `hx-get`/`hx-boost` attributes | **exact**, off `htmx.version` |
-| **jQuery** | `window.jQuery`, the `jQuery` data expando, the banner comment | **exact** |
-| **Stimulus** | `window.Stimulus`, `data-controller` | none |
-| **Ember.js** | `EmberENV`, `__ember_auto_import__`, the `ember-view` class | none |
-| **Backbone.js** | `window.Backbone` | **exact**, off `Backbone.VERSION` |
-| **Knockout** | `window.ko`, `__ko__` on DOM nodes | **exact**, off `ko.version` |
-
-### Bundlers
-
-| | Recognised by | Version |
-| --- | --- | --- |
-| **Vite** | `vite:preloadError`, `Unable to preload CSS for`, `__vite__mapDeps`, the `