Merge branch 'squelette-de-l-extension-firefox-cubi'
This commit is contained in:
@@ -26,9 +26,23 @@ relire et envoyer soi-même).
|
|||||||
|
|
||||||
## Installation (dev)
|
## Installation (dev)
|
||||||
|
|
||||||
Extension non signée, chargée comme module temporaire :
|
Extension non signée, chargée comme **module temporaire** (pas de build, pas de
|
||||||
`about:debugging` → *Ce Firefox* → *Charger un module complémentaire temporaire*
|
runtime à installer) :
|
||||||
→ sélectionner `manifest.json`. 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
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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") });
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Écho du Huit">
|
||||||
|
<rect width="96" height="96" rx="20" fill="#1d4ed8" />
|
||||||
|
<text x="48" y="50" font-family="system-ui, sans-serif" font-size="60" font-weight="700"
|
||||||
|
fill="#ffffff" text-anchor="middle" dominant-baseline="central">8</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 358 B |
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Écho du Huit</title>
|
||||||
|
<link rel="stylesheet" href="liste.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Écho du Huit</h1>
|
||||||
|
<ul id="liste"></ul>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="liste.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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);
|
||||||
@@ -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" }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user