diff --git a/README.md b/README.md index 435d7ba..e3c68ac 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,23 @@ relire et envoyer soi-même). ## Installation (dev) -Extension non signée, chargée comme module temporaire : -`about:debugging` → *Ce Firefox* → *Charger un module complémentaire temporaire* -→ sélectionner `manifest.json`. Pas de build, pas de runtime à installer. +Extension non signée, chargée comme **module temporaire** (pas de build, pas de +runtime à installer) : + +1. Ouvrir `about:debugging` → *Ce Firefox*. +2. *Charger un module complémentaire temporaire…* +3. Sélectionner `extension/manifest.json`. +4. Cliquer l'icône de la barre d'outils → la page de la liste s'ouvre dans un + onglet. + +> ⚠️ Un module temporaire **disparaît au redémarrage de Firefox** : il faut le +> recharger via `about:debugging` à chaque session (comportement de dev attendu). + +### Vérifier l'extension + +```sh +bunx web-ext lint --source-dir=extension +``` ## Documentation diff --git a/docs/solutions/2026-06-29-squelette-de-l-extension-firefox-cubi-solution.md b/docs/solutions/2026-06-29-squelette-de-l-extension-firefox-cubi-solution.md new file mode 100644 index 0000000..c7367d8 --- /dev/null +++ b/docs/solutions/2026-06-29-squelette-de-l-extension-firefox-cubi-solution.md @@ -0,0 +1,92 @@ +# Solution : Squelette de l'extension Firefox + +## Problème résolu +Poser la structure minimale d'une extension Firefox (Manifest V3, **sans build +step**, vanilla JS) chargeable en module temporaire via `about:debugging` sur +NixOS (pas de signature requise en dev). Le clic sur l'icône doit ouvrir une +**page dédiée autonome** — la future liste agrégeant l'Atelier du Huit et la +ville de Cugnaux. Ce squelette ne récupère aucune donnée : il câble la chaîne +icône → page. + +## Approche choisie +Entre les deux options du corps de tâche (`action.default_popup` **ou** page +dédiée), on retient la **page dédiée** ouverte dans un onglet via +`background.js` : + +- « liste autonome » = vraie page plein écran, lisible, rechargeable, non + contrainte aux ~600 px d'un popup qui se ferme à la perte de focus. +- Cela donne un rôle concret à `background.scripts` (clé Firefox explicitement + demandée) : écouter `action.onClicked` et faire `tabs.create`. +- Contrainte MDN confirmée : `action.onClicked` **ne se déclenche pas** si + `default_popup` est défini → on **n'ajoute pas** `default_popup`. + +Pas de dépendance runtime. Tests de forme avec `bun test` à la racine, +`web-ext lint` en contrôle dev optionnel via `bunx` (zéro dépendance ajoutée). + +## Décisions clés +- **Sous-dossier `extension/`** : isole l'extension chargeable (ce que voit + `about:debugging`) du tooling bun resté à la racine (tests, `package.json`). + Séparation des concerns nette. +- **`strict_min_version: "127.0"`** : avant Firefox 127, les `host_permissions` + étaient optionnelles (non accordées à l'install). Le seuil 127 garantit + qu'elles sont accordées dès l'installation. +- **Namespace `browser.*`** (API Firefox native, basée promesses) — jamais + `chrome.*`. Cohérent dans tout le code. +- **Permission `tabs` volontairement omise** : `tabs.create` ne la requiert + pas. Principe du moindre privilège. +- **Icône SVG unique** mappée sur les tailles `48`/`96` : accepté par Firefox, + low-tech. +- **`browser_specific_settings.gecko.id`** présent : identité stable de + l'extension. + +## Patterns à réutiliser +**Listener `onClicked` au niveau racine (synchrone)** — l'event page MV3 peut +être déchargée ; enregistrer le listener à la racine de `background.js` (jamais +dans un callback async) garantit qu'il est rattaché au réveil : +```js +// extension/background.js +browser.action.onClicked.addListener(() => { + browser.tabs.create({ url: browser.runtime.getURL("liste.html") }); +}); +``` +`browser.runtime.getURL(...)` plutôt qu'une URL concaténée : robuste et sûr. + +**Test de forme du manifeste (garde-fous structurels)** — `bun test` valide la +structure sans navigateur, dont le garde-fou anti-régression `default_popup` +absent : +```ts +const manifest = await Bun.file(join(extensionDir, "manifest.json")).json(); +test("ne définit PAS default_popup (sinon onClicked devient muet)", () => { + expect(manifest.action.default_popup).toBeUndefined(); +}); +test("demande les host_permissions des deux sources", () => { + expect(manifest.host_permissions).toEqual([/* URLs exactes, ordre vérifié */]); +}); +``` +Vérifier aussi l'existence des ressources référencées (`existsSync`). + +## Pièges à éviter +- **`default_popup` + `onClicked` incompatibles** : ajouter `default_popup` rend + `onClicked` muet → la page ne s'ouvre plus. Protégé par un test dédié. +- **`host_permissions` < FF127** : sans `strict_min_version: "127.0"`, elles ne + sont pas accordées à l'install. +- **Listener dans un callback async** : peut être manqué au réveil de l'event + page. Toujours au niveau racine. +- **Module temporaire volatil** : un add-on chargé via `about:debugging` + disparaît au redémarrage de Firefox → à recharger (nature dev, documentée + dans le README). +- **Ressource d'icône manquante** : si l'icône SVG n'est pas fournie, retirer + les clés `icons`/`action.default_icon` sinon erreur de ressource au lint. +- **Worktree sans baseline** : le worktree n'héritait pas du README/`docs/` + canoniques. Résolu par rebase sur `main` une fois la baseline réelle commitée + (`328df2d`). Penser à vérifier la baseline avant de dupliquer des fichiers. +- **Warning `MISSING_DATA_COLLECTION_PERMISSIONS`** (web-ext) : non bloquant + ici, mais Firefox recommande désormais + `gecko.data_collection_permissions: { "required": ["none"] }` pour une + extension sans collecte — à prévoir pour les prochaines. +- **Diagnostics IDE `bun:test`** : `bun test` tourne sans `tsconfig`, mais + l'éditeur signale `Cannot find module 'bun:test'` faute de `bun-types`. + Purement cosmétique (aucun impact runtime). + +## Tags +tags: [firefox-extension, manifest-v3, webextension, vanilla-js, bun-test, web-ext, low-tech, nixos] diff --git a/extension.test.ts b/extension.test.ts new file mode 100644 index 0000000..9dd970d --- /dev/null +++ b/extension.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +const extensionDir = join(import.meta.dir, "extension"); +const manifest = await Bun.file(join(extensionDir, "manifest.json")).json(); + +describe("manifest.json", () => { + test("est en Manifest V3", () => { + expect(manifest.manifest_version).toBe(3); + }); + + test("déclare background.js comme script d'arrière-plan", () => { + expect(manifest.background.scripts).toContain("background.js"); + }); + + test("demande les host_permissions des deux sources", () => { + expect(manifest.host_permissions).toEqual([ + "https://atelier-huit.frama.space/*", + "https://www.ville-cugnaux.fr/*", + ]); + }); + + test("ne définit PAS default_popup (sinon onClicked devient muet)", () => { + expect(manifest.action.default_popup).toBeUndefined(); + }); + + test("porte un identifiant gecko", () => { + expect(manifest.browser_specific_settings.gecko.id).toBeDefined(); + }); +}); + +describe("ressources référencées", () => { + test("background.js existe", () => { + expect(existsSync(join(extensionDir, "background.js"))).toBe(true); + }); + + test("liste.html existe", () => { + expect(existsSync(join(extensionDir, "liste.html"))).toBe(true); + }); + + test("l'icône référencée existe", () => { + expect(existsSync(join(extensionDir, manifest.action.default_icon))).toBe(true); + }); +}); diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..2431475 --- /dev/null +++ b/extension/background.js @@ -0,0 +1,5 @@ +// Listener enregistré au niveau racine (synchrone) : l'event page peut être +// déchargée, le clic toolbar doit toujours la réveiller et être capté. +browser.action.onClicked.addListener(() => { + browser.tabs.create({ url: browser.runtime.getURL("liste.html") }); +}); diff --git a/extension/icons/icon.svg b/extension/icons/icon.svg new file mode 100644 index 0000000..6f79713 --- /dev/null +++ b/extension/icons/icon.svg @@ -0,0 +1,5 @@ + + + 8 + diff --git a/extension/liste.css b/extension/liste.css new file mode 100644 index 0000000..5bfee0a --- /dev/null +++ b/extension/liste.css @@ -0,0 +1,29 @@ +:root { + color-scheme: light dark; + font-family: system-ui, sans-serif; +} + +body { + margin: 0; +} + +main { + max-width: 48rem; + margin: 0 auto; + padding: 2rem 1.5rem; +} + +h1 { + margin-top: 0; +} + +#liste { + list-style: none; + margin: 0; + padding: 0; +} + +.liste-vide { + color: #666; + font-style: italic; +} diff --git a/extension/liste.html b/extension/liste.html new file mode 100644 index 0000000..a19bc08 --- /dev/null +++ b/extension/liste.html @@ -0,0 +1,16 @@ + + + + + + Écho du Huit + + + +
+

Écho du Huit

+ +
+ + + diff --git a/extension/liste.js b/extension/liste.js new file mode 100644 index 0000000..10a2141 --- /dev/null +++ b/extension/liste.js @@ -0,0 +1,8 @@ +// Squelette : la récupération des deux sources (Atelier du Huit, ville de +// Cugnaux) arrivera dans des tâches ultérieures. Pour l'instant, état vide. +const liste = document.querySelector("#liste"); + +const vide = document.createElement("li"); +vide.className = "liste-vide"; +vide.textContent = "Aucune donnée pour l'instant."; +liste.append(vide); diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..8c45091 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,24 @@ +{ + "manifest_version": 3, + "name": "Écho du Huit", + "version": "0.1.0", + "description": "Liste autonome agrégeant les infos de l'Atelier du Huit et de la ville de Cugnaux.", + "browser_specific_settings": { + "gecko": { + "id": "echo-du-huit@atelier-huit", + "strict_min_version": "127.0" + } + }, + "background": { + "scripts": ["background.js"] + }, + "action": { + "default_title": "Ouvrir la liste de l'Écho du Huit", + "default_icon": "icons/icon.svg" + }, + "host_permissions": [ + "https://atelier-huit.frama.space/*", + "https://www.ville-cugnaux.fr/*" + ], + "icons": { "48": "icons/icon.svg", "96": "icons/icon.svg" } +}