@mister-guiiug/dev-pwa-config
Configurations partagées (ESLint, Prettier, TypeScript, Vitest) pour les
projets PWA de la famille miss-* et mister-*.
Distribué via GitHub Packages sur le registre npm.pkg.github.com.
Où lire quoi
Le manuel de ce socle ne tenait plus dans une page : 167 sous-chemins, neuf
binaires, onze workflows réutilisables. Il vit dans docs/, découpé par sujet,
et rien n'y a été résumé, seulement déplacé.
| Page | Ce qu'on y trouve |
|---|
docs/EXPORTS.md | La table des sous-chemins publiés — le point d'entrée quand on cherche « est-ce que le socle sait déjà faire ça ? » |
docs/CONFIGS.md | ESLint, Prettier, TypeScript, Vitest, Playwright, Vite (PWA, CSP, SEO), commitlint, lint-staged, icônes |
docs/INTERFACE.md | Composants React, habillage components.css, primitives, catalogue famille, Rive, accessibilité |
docs/DONNEES.md | Persistance, magasin versionné, coffre chiffré, Supabase, file hors-ligne, temps réel, carte, auth, PDF/Excel/iCal |
docs/BINS.md | Binaires pwa-* : doctor, bundle-budget, icons, pgtap, screenshots, og-image, bindings, typecheck-7 |
docs/MIGRATIONS.md | Ce qu'a demandé chaque majeure |
docs/V7.md | Périmètre v7 : catalogue des modules (apport + évolutions apps), types, foyer catalogue |
Et les dossiers d'analyse, qui ne sont pas des manuels : CAMPAGNE.md
(adoption), GISEMENTS.md (promotion), PARC.md (le
parc vu de dehors), STRATEGIE.md (bibliothèque, squelette,
générateur), AMELIORATIONS.md, VALEUR.md
(la valeur vue de l'utilisateur), CONFIG.md et
PARAMETRAGE.md (secrets et variables),
ESLINT-10.md.
Projets consommateurs
Tableau engendré depuis apps-catalog.js (npm run sync) : la colonne
« Sous-chemins consommés » est un RELEVÉ — les import et les extends trouvés
dans le code de chaque dépôt —, pas une intention. Deux choses s'y lisent tout
de suite : la part des dépôts qui reprennent components.css, et les
sous-chemins qui n'ont qu'un seul adoptant. Les comptes sont dans le tableau, et
nulle part ailleurs : écrits en toutes lettres ici, ils avaient vieilli.
⚠️ Ce tableau compte des sous-chemins, pas des composants. Une app qui
importe FamilyApps depuis le baril react n'y fait pas apparaître
react/family-apps : ce sous-chemin semble donc n'avoir que deux adoptants,
alors que seize applications importent le composant. Pour l'adoption d'un export,
c'est le relevé par symbole du
showroom qui répond.
| Projet | Persistance | Sous-chemins consommés |
|---|
miss-carbook | Supabase | components.css, eslint-react, image, lint-staged, playwright-a11y, playwright-base, prettier, react, react/confirm-dialog, react/empty-state, react/i18n, react/observability, react/sheet, react/toast, react/update-prompt-banner, react/use-online, react/use-update-prompt, realtime, realtime/supabase, tailwind-preset.css, tsconfig-app-react, tsconfig-node, vite-pwa-base, vitest-base, vitest-setup, web-vitals — 26 |
miss-contraction | Local-first | download, eslint-react, lint-staged, pdf, playwright-base, prettier, react, react/observability, react/use-wake-lock, tailwind-preset.css, tsconfig-app-react, tsconfig-node, vite-pwa-base, vitest-base, vitest-setup, web-vitals — 16 |
miss-genius | Local-first | apps-catalog, components.css, download, eslint-react, format, lint-staged, playwright-a11y, playwright-base, prettier, react, react/app-footer, , , , , , , , , , , , , , , , , , , — |
Adoption réelle
Le tableau ci-dessus dit ce que chaque app importe. Celui-ci dit l'inverse — ce
qu'elle n'importe pas alors que le paquet le fournit, parce qu'elle en garde
une copie. C'est le seul chiffre qui mesure l'utilité de ce dépôt, et il n'était
écrit nulle part.
Ce qu'il montre, au relevé du 30 août : les deux couches sont adoptées. La
couche outillage l'était déjà (vitest-base, l'observabilité, Playwright,
les greffons Vite) ; la couche interface a suivi quand components.css —
son prérequis, longtemps pris par trois apps sur dix-sept — est passé à
quinze. ConfirmDialog et ErrorBoundary étaient alors importés par dix
apps, EmptyState par sept, Sheet par six.
Sept postes sont passés à zéro en deux jours : UpdatePromptBanner (8
copies), links (7), applyUpdate (6), format (5), Toast, ThemeToggle,
ErrorBoundary. Le dernier, applyUpdate, était le plus coûteux : il ne
supposait pas un import mais de reprendre un enregistrement de service worker,
la désinscription de développement que cinq apps avaient écrite, et — pour trois
d'entre elles — de trancher ce que registerType: 'autoUpdate' permet
réellement. Ce qui restait demandait la même chose : useI18n (4) un
fournisseur et des dictionnaires, useTheme (3) un état à déplacer.
Le chiffre est passé de 71 à 17, et il faut séparer les causes. En rejouant
l'ancienne règle sur un état intermédiaire, on obtenait 58 là où la nouvelle
disait 46 : douze doublons étaient donc invisibles à l'instrument — neuf
besoins qu'il ne savait pas acquitter, trois façades qu'il comptait comme des
réécritures. Le reste de la baisse est du travail réel. Confondre les deux
attribuerait à la campagne ce que personne n'a fait (voir CAMPAGNE.md). Au
relevé du 07/09, il ne restait que trois fichiers (links ×2, Stat ×1) —
fermé le 04/10/2026. Le tableau ci-dessous doit rester à zéro dans la colonne
« Encore recopié ».
Et il SOUS-ESTIME encore la dette. Il compte les copies d'un catalogue de
besoins déjà nommés : une app qui réécrit quelque chose que le catalogue
ignore ne compte pour rien. Treize réécritures à la main ont disparu le même
jour — quatre agendas iCalendar, trois traitements d'image, trois verrous
d'écran, un classeur Excel — sans peser un point, parce qu'aucune ne portait
un nom du catalogue. Le relevé mesure la migration de ce qu'on sait déjà
partagé ; il ne découvre rien. Ce qui découvre, c'est de lire les apps — et
c'est ainsi que ces quatre modules sont nés.
Le sens inverse a son instrument depuis le 02/09/2026.
node scripts/promotion-candidates.mjs sort ce que plusieurs apps écrivent et
que le paquet n'exporte pas, avec pour chaque exemplaire son nombre
d'importateurs (zéro : un cadavre) et pour chaque groupe sa similarité (1,00 :
une copie). Le tri qui en est sorti — dix chantiers classés, et ce qu'on ne
fait pas — est dans GISEMENTS.md.
node scripts/probe-sites.mjs lit les sites PUBLIÉS du catalogue (manifeste, CSP,
Open Graph, repli 404, poids du JS initial) ; node scripts/dead-exports.mjs
relève les exports que personne n'appelle. Ce que ces sondes ont trouvé le
02/09/2026 — Renovate jamais actif, une app non installable, un relevé
d'adoption qui comptait 19 copies pour 34 — est classé dans PARC.md.
Ce que le socle INTERDIT encore, depuis le 10/09/2026.
node scripts/plafonds.mjs compare chaque plage déclarée (peerDependencies,
et --dev pour la chaîne de développement) à la version publiée sous
l'étiquette latest, et ne retient que les plafonds qui excluent la majeure
courante. Une peerDependency est un plafond pour les vingt apps, et un
plafond ne fait aucun bruit : il se découvre le jour où quelqu'un tente de
monter, en conflit de peers, très loin d'ici. Le premier relevé en a trouvé
neuf, dont eslint@^9 sorti du support ; le workflow Plafonds le rejoue
chaque lundi et sur toute PR qui touche package.json. Il reste vert : un
plafond assumé n'est pas un défaut — il doit seulement rester une décision.
Relevé du 2026-10-01 sur 21 dépôts, par npm run adoption.
| Export ou module | Importé par | Encore recopié dans |
|---|
baseTestOptions | 20 / 21 | — |
pwaSeoPlugin | 20 / 21 | — |
UpdatePromptBanner | 20 / 21 | — |
versionPlugin | 20 / 21 | — |
ConfirmDialog | 16 / 21 | — |
createLogger | 16 / 21 | — |
cspPlugin | 16 / 21 | — |
FamilyApps | 16 / 21 | — |
initSentry | 16 / 21 | — |
installErrorReporter | 16 / 21 | — |
repoUrl | 16 / 21 | — |
AppFooter | 15 / 21 | — |
definePwaPlaywrightConfig | 15 / 21 | — |
useTheme | 15 / 21 | — |
expectNoA11yViolations | 14 / 21 | — |
applyUpdate | 13 / 21 | — |
createI18n | 13 / 21 | — |
getDefaultLocale | 13 / 21 | — |
ThemeProvider | 13 / 21 | — |
ConnectionBanner | 12 / 21 | — |
EmptyState | 12 / 21 | — |
recordError | 12 / 21 | — |
Sheet | 12 / 21 | — |
useActionGuard | 12 / 21 | — |
AppUpdates | 11 / 21 | — |
|
Showroom du design system
showroom/ est une page statique (HTML + CSS + JS, aucune dépendance,
aucun build, aucune requête réseau) qui présente ce que le paquet partage
réellement :
- les tokens du preset — typographie et espacements fluides, points de rupture,
safe-areas iOS, cible tactile — avec leurs valeurs calculées en direct
(redimensionner la fenêtre fait jouer les
clamp()) ;
- le DOM exact de chaque composant
/react et les sélecteurs
[data-dwc="…"] correspondants ;
- une vitrine des applications de la famille — le catalogue, pas une copie —,
en grille ou en tableau :
recherche sans diacritiques (les facettes et les sous-chemins y sont
cherchables : « supabase », « vite-csp »), quatre axes de filtres croisés
affichant le compte qu'ils donneraient, tri, ancre par application, liens app
- dépôt, et un bouton qui rhabille la page entière avec la palette de l'app.
La grille est engendrée depuis
apps-catalog.js — le fichier qu'importent
les apps pour s'afficher les unes les autres. Le filtre Consomme répond à
la question qu'un design system doit se poser en premier : qui utilise
vraiment quoi ? (la vitrine calcule le compte de components.css depuis le
catalogue, au rendu) ;
- un catalogue cherchable de tout ce que le paquet exporte — composants et
hooks —, dont
test/showroom-catalogue.test.mjs vérifie qu'il ne laisse
échapper aucun export de react/index.js ;
- des pièges par composant, tirés de défauts constatés et non de principes
(7 apps sur 13 avaient réimplémenté
EmptyState, les variantes sm locales
descendaient à 32 px…), et une note d'accessibilité par fiche ;
- des arbres de décision pour les cas où deux composants conviennent :
signaler un problème, occuper une attente, demander une saisie, dire un état ;
- un sélecteur de thème qui rhabille toute la page avec l'univers visuel de
chaque application consommatrice, plus le contrat clair / sombre / système du
hook
useTheme. La section Démo aligne d’abord toutes les palettes :
chaque tuile est peinte avec celle de l’application, et un clic est le même
geste que « Habiller la page » ;
- des contrôles d'accessibilité calculés sur la page — cible tactile mesurée
et contraste WCAG par paire —, rejoués à chaque bascule de thème ;
- une section Stack relevée dans le code des apps : Supabase / Firebase /
local-first et leurs fonctionnalités réellement appelées, icônes, cartes,
outillage de test ;
- une bascule français / anglais. Le français est le HTML lui-même, capturé
au chargement ;
showroom/i18n.js ne porte que les autres langues, et
refuse qu'un bloc reste sans traduction.
refuse qu'une traduction survive à
un changement du français : chaque clé anglaise garde l'empreinte du
français qu'elle traduit (). Ce second
test lit la page avec jsdom ; le job de publication, qui n'installe rien,
rejoue le premier seulement. signale une empreinte
périmée sans la réécrire ; une fois l'anglais relu,
l'acquitte.
npm run showroom
→ http://127.0.0.1:5220/dev-pwa-config/, sous le même chemin que sur Pages.
Un serveur est nécessaire : la page charge des modules ES (showroom.js,
les modules qu'il importe et la copie de command.js), que Chromium et
Firefox refusent en file://. showroom.js n'est que l'amorçage : chaque
partie de la page a son module (vitrine.js, habillage.js, scenes.js,
recherche.js…), et l'état qu'elles partagent vit dans etat.js.
Ouvert d'un double-clic, index.html n'affiche que son texte et ses styles,
sans rien de ce que le script engendre (vitrine, catalogue, palettes).
La page porte une Content-Security-Policy en <meta> (Pages n'accepte pas
d'en-têtes) : scripts et feuilles seulement sous
https://mister-guiiug.github.io/dev-pwa-config/, aucune requête réseau
(connect-src 'none'), ni <base>, ni envoi de formulaire, ni plugin. Le
showroom partage son origine avec toutes les apps : une injection de script ici
lirait leur localStorage. L'empreinte du script en ligne est écrite par
npm run sync et vérifiée par test/showroom-csp.test.mjs ; npm run showroom
tourne la politique vers son adresse locale. Pour la même raison d'origine
partagée, le choix clair / sombre est rangé sous dwc_showroom_scheme, et non
sous dwc_theme, la clé que lisent les apps.
Le preset n'expose aucune couleur : c'est la part variable, propriété de
chaque app. Le thème « Générique » du showroom est donc volontairement
monochrome ; les palettes des applications sont relevées dans themes.js
(sous-chemin ./themes), dont showroom/themes.js est un miroir engendré.
Sans compilateur Tailwind, une page statique ne peut pas interpréter @theme
ni @utility : showroom/preset.css rejoue donc le preset en CSS natif.
test/showroom.test.mjs compare les deux fichiers token par token — une
modification du preset non répercutée fait échouer la CI, pas le navigateur.
Même raison pour le catalogue : la page ne charge que des fichiers de son
dossier, le seul que publie Pages. showroom/apps.js
(globalThis.SHOWROOM_APPS), showroom/components.css et
showroom/command.js sont donc engendrés depuis la racine :
npm run sync # scripts/sync-generated.mjs
npm run sync régénère tout ce que le dépôt tient en double :
showroom/apps.js et showroom/themes.js (miroirs du catalogue et des
palettes), la copie showroom/components.css et les morceaux publiés
components/*.css (tous deux tirés de components.css), la copie
showroom/command.js (la recherche Ctrl+K du paquet), les tableaux « Projets
consommateurs » et « Adoption réelle »
ci-dessus, et trois choses que la page ne peut pas tenir à la main :
l'empreinte CSP de son script en ligne, un ?v=<empreinte du contenu> sur
chaque fichier qu'elle charge (Pages sert dix minutes en cache, sous un nom
fixe : sans empreinte, une page neuve pouvait tourner avec un script ancien),
et ses démos de composants, rendues par les vrais composants de react/
(scripts/showroom-demos.mjs). Écrites à la main, la barre d'onglets avait
perdu son aria-current et l'en-tête son h1, sans que rien le dise ;
test/showroom-demos.test.mjs refuse désormais une démo périmée, et tient la
liste de celles encore écrites à la main, chacune avec sa raison : elle ne peut
que raccourcir.
La CI relance npm run sync et refuse le moindre écart ;
test/apps-catalog.test.mjs compare en outre le catalogue à ses dérivés, et
vérifie que les comptes annoncés par la section « Stack » (« 9 apps » Supabase,
« 3 apps » Firebase, « 5 apps » local-first) collent toujours au champ backend.
Deux relevés complètent la vitrine, et ne sont pas dans sync parce qu'ils
demandent un accès réseau :
npm run screenshots # captures des apps déployées → showroom/screenshots/
npm run screenshots -- miss-dice
node scripts/fetch-metrics.mjs # état des dépôts → showroom/metrics.js
Le second est rejoué au moment de publier par showroom-pages.yml :
version publiée, date du dernier push, dépôt archivé. Ce qui part dans
l'artefact est donc frais par construction. La page ne fait toujours aucune
requête — le relevé est posé sur globalThis par un <script src>, comme
themes.js. Un fichier vide est un état valide : la vitrine n'affiche alors
simplement aucune mesure.
Le showroom/metrics.js du dépôt sert la lecture locale, depuis un clone
(npm run showroom). Il n'est plus rafraîchi
automatiquement, et la vitrine affiche la date de son relevé pour que personne
ne la prenne pour celle du jour.
Le relevé n'est plus commité, et c'est la trace d'une panne.
showroom-metrics.yml poussait showroom/metrics.js directement sur main.
Le ruleset « Protect main », posé le 01/09/2026, refuse ce push. Le job a
échoué douze nuits de suite sans que rien ne s'arrête : la page s'affichait
toujours, avec des mesures figées. Produire le fichier à la publication
supprime le besoin d'écrire sur une branche protégée.
showroom-metrics.yml ne garde que la cadence : chaque nuit, il relève,
compare à ce que la page en ligne affiche — pas au dépôt — et ne demande
la publication que si l'état des dépôts a bougé.
Stack cible (septembre 2026)
Les configs imposent / supposent les versions suivantes côté projet consommateur :
Node ≥22 promis par engines ; le parc épingle 26.13.0 (.nvmrc, setup-pwa, réutilisables) ; CI du socle éprouvée sur 22 ET 26.13.0
TypeScript ~6.0.3 strict + verbatimModuleSyntax + noUncheckedIndexedAccess, cible ES2025 + lib ES2025
ESLint 9 OU 10 (flat config) + typescript-eslint 8.58 — la 10 demande trois gestes côté app (ESLINT-10.md)
eslint-plugin-react-hooks 7.0 (configs.flat.recommended) + eslint-plugin-react-refresh 0.5
Vite 8 (Rolldown) + Vitest 4 ou 5 (jsdom + globals + setupFiles)
Zod 4 (peer)
Prettier 3.6 (singleQuote, tabWidth 2, printWidth 80, trailingComma es5, arrowParens 'avoid')
Tailwind 4 (@tailwindcss/vite) + lucide-react (icônes — standard famille)
3.0.0 (breaking) — tsconfig-app/tsconfig-node activent
verbatimModuleSyntax et noUncheckedIndexedAccess (de nouvelles
erreurs TS peuvent apparaître au bump — cf. migration 3.0.0),
et engines.node passe à >=22.
2.0.0 (breaking) — les peer-dependencies passent en Vite 8 / Vitest 4 / TypeScript ~6.0.3 / Zod 4
(plus de support Vitest 3 ni Zod 3). Voir la migration.
Icônes — lucide-react (règle famille)
Les projets React de la famille utilisent lucide-react comme bibliothèque
d'icônes d'interface (navigation, boutons d'action, tendances, en-têtes). C'est
le standard partagé : cohérence visuelle entre miss-* / mister-*, icônes
SVG tree-shakées (on n'embarque que celles importées), strokeWidth/size
ajustables, et currentColor qui suit les tokens du thème.
npm install lucide-react
import { Plus, Trash2 } from 'lucide-react';
// Icône décorative -> aria-hidden ; le libellé accessible vit sur le bouton.
<button aria-label="Supprimer">
<Trash2 size={18} aria-hidden="true" />
</button>;
Conventions :
- Décoratives :
aria-hidden="true" + un libellé porté par le parent
(aria-label, texte visible…). Ne jamais s'appuyer sur la seule icône.
- Tailles :
size={18} (boutons/inline), size={22} (nav), size={13}
(pastilles). Couleur via className (text-primary, currentColor).
- Emoji autorisé uniquement pour le contenu utilisateur (ex. icône de
matière choisie) et les illustrations « mascotte » (états vides, onboarding),
pas pour l'iconographie fonctionnelle.
Alternative SVG inline (assumée). lucide-react est le standard quand
une app a des icônes fonctionnelles, mais il n'est pas obligatoire : une app
peut inliner ses SVG (cf. miss-badminton, src/react/components/icons.tsx)
pour garder un bundle minimal. Dans ce cas, ne pas déclarer lucide-react
dans package.json. La règle ferme reste : pas d'icône de marque via lucide
(la 1.x ne les fournit plus) → logo GitHub en SVG inline, Coffee pour le sponsor.
Liens app — code source, sponsor, signalement (règle famille)
Chaque application de la famille expose trois liens : son code source
(dépôt GitHub), un lien sponsor (Buy Me a Coffee) et « Signaler un
problème » (AppFooter issues : le gabarit bug.yml du compte, prérempli
avec la version, le commit, l'écran et le navigateur). Transparence (apps
gratuites, locales, open source), soutien, et retour.
OÙ — la règle, depuis le 06/09/2026. Les trois liens sont visibles sur
deux écrans, et deux seulement : l'accueil, et À propos ou Réglages.
Pas l'un ou l'autre : les deux — qui ouvre l'app doit pouvoir vérifier ce
qu'elle fait et remercier sans aller les chercher dans un tiroir, et qui vient
les chercher doit les trouver là où on range ce genre de chose. Et nulle
part ailleurs : un pied de page qui suit chaque écran pèse sur un plateau de
jeu, un formulaire, une carte — trois liens sortants sous une saisie, ce n'est
pas un pied de page, c'est du bruit.
La coquille n'est plus une façon de la tenir. <AppFooter> rendu hors des
<Routes> est sur TOUS les écrans : c'était la réponse du socle la veille,
c'est aujourd'hui un écran de trop. Le pied de page se rend dans l'écran
d'accueil et dans À propos / Réglages — deux fichiers, le même composant.
npx pwa-doctor le vérifie (liens-famille) : il dépouille les routes,
résout une indirection (<Footer/> défini à part, rendu par la coquille ou
par deux écrans), lit la condition d'une coquille sans routeur, et reconnaît
l'accueil et les réglages au nom de fichier. Relevé du 06/09/2026 : une app
sur vingt tient la règle — mister-molkky, par deux écrans. Seize rendent
le pied de page par la coquille, donc partout (miss-badminton,
miss-carbook, miss-dice, miss-genius, miss-lookhouse, miss-supaboss,
miss-supatool, miss-ticket-pwa, miss-uwh, mister-doc,
mister-family-map, mister-footcoach, mister-miss-koh, mister-puzzle,
mister-qowa et le squelette) ; deux sur trois écrans (miss-contraction :
la liste de contrôle en plus ; mister-cim10 : l'aide ET les réglages) ; une
sans aucun lien (mister-quota).
Deux niveaux, à mettre en place ensemble :
-
Dans l'app — un src/links.ts centralise les URL, consommé par le pied
de page des deux écrans :
// src/links.ts
export const REPO_URL = 'https://github.com/mister-guiiug/<projet>';
export const SPONSOR_URL = 'https://buymeacoffee.com/mister.guiiug';
// Footer : lien source + sponsor (cibles externes sécurisées).
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">Code source</a>
<a href={SPONSOR_URL} target="_blank" rel="noopener noreferrer">
<Coffee size={16} aria-hidden="true" /> M'offrir un café
</a>
Ou, sans écrire ni URL ni balise — la forme du squelette, et celle que
pwa-doctor reconnaît sans rien deviner :
// src/features/home/HomeScreen.tsx — et la même ligne dans AboutScreen.tsx
// (ou SettingsScreen.tsx). Jamais dans App.tsx hors des <Routes> : là, il
// serait sur tous les écrans.
export function HomeScreen() {
return (
<>
{/* … */}
{/* Le lien de soutien n'est pas passé : AppFooter le prend au catalogue. */}
<AppFooter repoUrl={REPO_URL} issues />
</>
);
}
Un <AppFooter> écrit dans la coquille, hors des <Routes>, vaut pour
toutes les routes — celles où il n'a rien à faire comprises. C'était la forme
de seize apps le 06/09/2026, et elle est invisible : la page où on le
regarde, c'est justement une de celles qui doivent l'avoir.
Le lien de soutien n'a pas à être écrit : AppFooter et FamilyApps
prennent déjà celui de la famille. Pour le remplacer, le déclarer une
fois —
import { SponsorProvider } from '@mister-guiiug/dev-pwa-config/react/sponsor';
<SponsorProvider handle="autre.pseudo"> {/* autre pseudo BMC */}
<SponsorProvider url="https://liberapay.com/…"> {/* autre plateforme */}
<SponsorProvider url={null}> {/* aucun lien */}
Trois niveaux, comme LabelsProvider : la prop l'emporte, puis le contexte,
puis la famille. null n'est pas undefined — c'est « pas de lien », et il
est respecté : sans quoi un fork ne pourrait pas retirer un appel au don qui
pointe vers quelqu'un d'autre.
-
Sur le dépôt — .github/FUNDING.yml active le bouton « Sponsor » de
GitHub. Template prêt à copier : templates/FUNDING.yml.
buy_me_a_coffee: mister.guiiug
Conventions :
- Liens externes : toujours
target="_blank" + rel="noopener noreferrer".
- Marque GitHub :
lucide-react 1.x ne fournit plus d'icônes de marque —
utiliser un SVG inline pour le logo GitHub ; Coffee (lucide) pour le
sponsor. Handle sponsor unique de la famille : mister.guiiug.
Installation (GitHub Packages)
Étape 1 — .npmrc à la racine du projet consommateur
@mister-guiiug:registry=https://npm.pkg.github.com
Étape 2 — Dépendance dans package.json
{
"devDependencies": {
"@mister-guiiug/dev-pwa-config": "^6.18.0",
},
}
Étape 3 — Authentification
Le paquet est publié sur le registre GitHub Packages, qui exige une authentification (même pour les paquets publics).
En local (machine de développement)
Créer un Personal Access Token avec read:packages uniquement, puis :
# Option 1 : npm login
npm login --scope=@mister-guiiug --auth-type=legacy --registry=https://npm.pkg.github.com
# Username = votre login GitHub
# Password = le PAT
# Option 2 : variable d'environnement
echo "//npm.pkg.github.com/:_authToken=ghp_xxxxxxxxxxxx" >> ~/.npmrc
En CI (GitHub Actions)
Le secrets.GITHUB_TOKEN automatique d'Actions suffit à lire le paquet, à condition que le workflow accorde permissions: packages: read (c'est ce que font les réutilisables, par la composite setup-pwa). Configuration type :
- uses: actions/setup-node@v7
with:
node-version: '26.13.0'
cache: npm
registry-url: 'https://npm.pkg.github.com'
scope: '@mister-guiiug'
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Les pairs optionnelles, à déclarer par l’app
GitHub Packages ne sert pas peerDependenciesMeta. Le tarball du socle le
porte, les métadonnées que lit npm non (relevé du 01/10/2026 ; le support de
GitHub classe ce manque en demande d’évolution depuis 2023, voir
la discussion #51104).
npm tenait donc pour OBLIGATOIRES les pairs que le socle disait optionnelles,
et les installait dans chaque app : chez mister-miss-koh, 125 entrées de
verrou et 228 Mo de node_modules, dont Firebase et @grpc/grpc-js.
Depuis le 01/10/2026, le socle déclare donc :
- en
peerDependencies, ses 11 pairs obligatoires : la chaîne ESLint,
Prettier, TypeScript, Vitest, et web-vitals, que l’observabilité importe ;
- en
optionalPeers, un champ que npm ignore, ses 23 pairs optionnelles
et leur plage, que scripts/plafonds.mjs et migrate-consumers.mjs --peers
relisent.
Une app déclare elle-même ce que ses modules attendent. Sans le paquet, le
build échoue en le nommant (failed to resolve import) ; hors de la plage,
rien ne prévient : la table dit ce qui est éprouvé.
| Paquet | Plage | Pour |
|---|
@axe-core/playwright | ^4.8.0 | playwright-a11y, qui reçoit AxeBuilder de l’app |
@commitlint/cli | ^19.0.0 || ^20.0.0 || ^21.0.0 | le hook commit-msg |
@commitlint/config-conventional | ^19.0.0 || ^20.0.0 || ^21.0.0 | commitlint |
@playwright/test | ^1.49.0 | playwright-base, playwright-a11y, playwright-entree, et les bins pwa-screenshots, pwa-og-image |
@rive-app/react-canvas | ^4.36.0 | react/rive |
@sentry/react | ^10.75.2 || ^11.0.0 | react/observability, par son loader |
@supabase/supabase-js | ^2.0.0 | supabase-client |
@tanstack/react-query | ^5.0.0 | react/query-client |
@testing-library/jest-dom | ^6.0.0 || ^7.0.0 | vitest-setup |
@vitest/browser | ^4.0.0 || ^5.0.0 | vitest-browser-base |
firebase | >=9.0.0 | realtime/firebase, push/firebase, qui reçoivent les objets de l’app |
leaflet | ^1.9.0 | map/leaflet |
lucide-react | ^1.0.0 | les icônes que l’app passe aux composants |
maplibre-gl | ^6.0.0 | map/maplibre |
playwright | |
Monter à ESLint 10
ESLint 9 est sorti du support (npm le dit à chaque installation). Le socle
autorise la 10 depuis la 4.10.0 : ses peers eslint et @eslint/js acceptent
^9.39.4 || ^10.0.0, et il tourne lui-même en 10.
Trois gestes, pas deux — et c'est la correction que la passe du 10/09/2026 a
apportée au dossier ESLINT-10.md :
"devDependencies": {
"eslint": "^10.10.0",
"@eslint/js": "^10.0.1"
},
"overrides": {
"eslint-plugin-jsx-a11y": { "eslint": "$eslint" }
}
L'override lève la déclaration périmée de eslint-plugin-jsx-a11y, seul paquet
de la chaîne à s'arrêter à ^9 alors que son code fonctionne sous la 10. Mais
$eslint désigne la plage que le projet racine déclare lui-même : sans les
deux lignes de devDependencies, il ne renvoie à rien et l'override est inerte.
C'est ce qui a fait échouer la première tentative — le squelette ne déclarait pas
eslint, le laissant s'installer par la peer du socle.
Qui doit agir, et quand. Une app qui déclare déjà eslint@^9.39.4 n'est
touchée par rien : elle monte quand elle veut. Une app qui s'en remet à la peer
du socle cassera à son prochain npm install — pour elle, les trois gestes ne
sont pas optionnels, ils sont la condition d'une installation qui résout. Le
squelette pwa-starter-kit les porte, et sert de modèle.
Le coût mesuré reste la seule règle qui morde, no-useless-assignment : dix
occurrences dans le socle, toutes corrigées, zéro dans le squelette.
Secrets et variables — la ligne de partage
La question n'est pas « est-ce sensible ? », c'est « le navigateur le
voit-il ? ». Vite copie la valeur de tout VITE_* dans le bundle au moment du
build : elle part en clair sur GitHub Pages, lisible par n'importe qui. La
ranger dans un secret GitHub ne la protège donc de rien — ça masque seulement
les journaux de CI (***) et donne l'illusion d'une confidentialité qui
n'existe pas.
| Ranger en… | Quoi | Exemples |
|---|
secrets (chiffrés) | Ce qui donne un pouvoir : écrire, déployer, administrer. Jamais lu par le navigateur. | SUPABASE_ACCESS_TOKEN, SUPABASE_DB_PASSWORD, SUPABASE_DB_URL, FIREBASE_TOKEN, FIREBASE_SERVICE_ACCOUNT_KEY, CLOUDFLARE_API_TOKEN, RENOVATE_TOKEN |
vars (en clair, lisibles) | Ce qui finit dans le bundle ou dans une URL publique — donc tout VITE_*, et la configuration d'environnement. | VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, VITE_FIREBASE_*, VITE_VAPID_PUBLIC_KEY, VITE_SENTRY_DSN, VITE_BASE_PATH, SUPABASE_PROJECT_ID |
Deux clés méritent un mot, parce qu'elles ressemblent à des secrets :
- l'
anon key Supabase est un JWT de rôle anon, conçu pour être publié :
c'est la RLS qui protège les données, pas la clé. La vérifier plutôt que la
cacher — echo "$KEY" | cut -d. -f2 | base64 -d doit dire "role":"anon",
jamais "role":"service_role" ;
- les
VITE_FIREBASE_* sont la configuration publique du projet ; c'est
App Check et les règles de sécurité qui protègent, pas leur discrétion.
Comment les injecter
Le build lit les VITE_* par l'entrée build-env du réutilisable, une par
ligne — jamais par secrets: inherit, qui donnerait au workflow appelé tout
le trousseau du dépôt alors qu'il n'a besoin de rien :
jobs:
deploy:
uses: mister-guiiug/dev-pwa-config/.github/workflows/pwa-deploy.yml@v6
with:
use-base-path: true
build-env: |
VITE_SUPABASE_URL=${{ vars.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY=${{ vars.VITE_SUPABASE_ANON_KEY }}
# Celles dont l'absence CASSE l'app. Le déploiement s'arrête en les
# nommant, au lieu de publier un site au backend injoignable.
required-env: |
VITE_SUPABASE_URL
VITE_SUPABASE_ANON_KEY
# Nommés, jamais hérités : le workflow ne reçoit que ce qu'il déclare.
# Ne lister que ceux que `pwa-deploy.yml` déclare et que l'app utilise —
# onze des douze appelants en `inherit` ne lui passaient rien.
secrets:
FIREBASE_SERVICE_ACCOUNT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY }}
required-env est le garde qui manquait. L'injection ne contrôle que la
forme : quand vars.VITE_SUPABASE_URL n'existe pas, la ligne vaut
VITE_SUPABASE_URL= — elle passe, et le build reçoit une chaîne vide. C'est
ainsi que mister-qowa a été publié avec apiKey: undefined. N'y lister que les
variables sans repli : une VITE_SENTRY_DSN absente fait taire
l'observabilité, elle ne casse rien, et un garde bruyant finit désactivé.
Ce qui doit rester vrai
- Toute
VITE_* que le code lit figure dans .env.example, avec un
commentaire disant à quoi elle sert et si elle est facultative. C'est la seule
documentation qu'un nouveau venu lira.
- Une app doit démarrer sans configuration. miss-lookhouse, miss-uwh et
mister-footcoach retombent sur un backend
local quand l'URL ou la clé
manquent — la démo publique fonctionne, hors ligne, sans compte. À l'inverse,
une app qui construit apiKey: import.meta.env.VITE_… sans repli se déploie
silencieusement cassée : le site est en ligne, son backend est
injoignable, et rien ne le dit.
pwa-doctor relève les écarts : VITE_* rangée en secret, secrets: inherit, .env.example absent ou incomplet.
Ces règles n'étaient pas appliquées, et la raison est instructive : relevé
du 04/09/2026, douze des seize workflows de déploiement héritaient du trousseau
entier et quinze valeurs publiques dormaient en secrets, parce que le gabarit
qu'on copiait disait, en commentaire, d'y ranger les VITE_*. Il enseigne
désormais vars, et pwa-doctor relève les deux écarts là où il tourne
(secrets-inherit, vite-en-secret). Le modèle qui
rend la règle mécanique plutôt que documentaire — un manifeste déclaré, un
.env.example engendré, un audit qui confronte la déclaration à l'API GitHub,
et trois gardes pour qu'une valeur absente n'atteigne jamais la production en
silence — est instruit dans CONFIG.md. La procédure, geste par
geste, avec ses variantes (Supabase, Firebase, Cloudflare, le socle, le
miroir, déplacer une valeur, retirer) est dans PARAMETRAGE.md.
Nouveau projet : une commande
npx github:mister-guiiug/create-lg-pwa-app miss-exemple --publish
create-lg-pwa-app tire
le squelette pwa-starter-kit,
substitue l'identité, écrit le lockfile avec npm 10 — celui du runner —,
fait le premier commit, crée le dépôt public et active Pages par un PUT.
C'est la voie recommandée depuis le 05/09/2026. Ce que la checklist manuelle
ci-dessous ne pouvait pas donner : le squelette apporte aussi la
composition — pile de fournisseurs, routeur, écrans de cadre, i18n,
sélecteur de backend, mise à jour du service worker — qui pesait 22 % des
lignes du parc et se réécrivait à chaque naissance, ainsi que sept décisions
d'architecture déjà prises.
Restent deux gestes, volontairement hors du générateur :
node scripts/apply-rulesets.mjs <id> pour protéger la branche, et une PR sur
apps-catalog.js sans laquelle l'application n'apparaît pas chez ses sœurs.
Checklist — à la main, ou pour comprendre ce que fait le générateur
.npmrc (copier templates/.npmrc) + .nvmrc
(26.13.0, l'épingle du parc ; engines promet >=22).
- Dépendance :
npm i -D @mister-guiiug/dev-pwa-config@^6 + les peers utilisés
(cf. peerDependencies du package.json : eslint, @eslint/js,
typescript-eslint, eslint-plugin-react-hooks, eslint-plugin-react-refresh,
globals, prettier, typescript, vite, vitest, react, tailwindcss…).
- Re-exports (une ligne chacun, cf. docs/CONFIGS.md) :
eslint.config.js,
prettier.config.js, commitlint.config.js, lint-staged.config.js.
- TypeScript :
tsconfig.app.json + tsconfig.node.json en extends.
- Tests :
vitest.config.ts (baseTestOptions) + src/test/setup.ts
(import '@mister-guiiug/dev-pwa-config/vitest-setup').
- CI/CD (secrets passés NOMMÉMENT — jamais
inherit — + permissions au niveau caller) : ci.yml →
pwa-ci.yml@v6, deploy.yml → pwa-deploy.yml@v6, lighthouse.yml →
pwa-lighthouse.yml@v6.
- PWA/SEO :
index.html depuis le squelette pwa-starter-kit (plus depuis
templates/) + pwaSeoPlugin + cspPlugin dans vite.config.ts. Préférer
npx create-lg-pwa-app à une copie manuelle.
- Famille : (écran Réglages/À propos) + .
Reusable workflows GitHub Actions
Hébergés dans .github/workflows/ — utilisables par tous les repos de la famille.
| Workflow | Rôle | Exemple d'appel |
|---|
pwa-ci.yml | Lockfile · Format · Lint · Type · Test · Build · audit npm de production (actif par défaut, seuil high) (+ docteur et E2E optionnels) | voir Utilisation |
pwa-deploy.yml | Build + déploiement GitHub Pages (avec VITE_BASE_PATH auto et repli SPA 404.html) | voir Utilisation |
npm-publish.yml | Publication npm sur GitHub Packages avec --provenance | voir Utilisation |
pwa-lighthouse.yml | Build + Lighthouse CI (perf/a11y/bp/seo) sur PR | uses: …/pwa-lighthouse.yml@v6 depuis un caller à pull-requests: write (requiert .lighthouserc.json, cf. template) |
pwa-supabase-migrate.yml | supabase link + db push (+ Edge Functions en option), sans annulation d'un run en cours ; quatre copies en une | uses: …/pwa-supabase-migrate.yml@v6 avec trois secrets : SUPABASE_ACCESS_TOKEN, SUPABASE_DB_PASSWORD, SUPABASE_PROJECT_ID (ce dernier en vars) (cf. en-tête, PARAMETRAGE.md) |
pwa-supabase-test.yml | Tests pgTAP sur une pile Supabase JETABLE du runner : migrations depuis zéro, aucun secret ; promu de miss-lookhouse, seule app à le faire. Complément du bin pwa-pgtap, qui joue les mêmes fichiers contre la base liée | uses: …/pwa-supabase-test.yml@v6 depuis un caller sur supabase/** (cf. en-tête du fichier ; exige supabase/config.toml) |
|
Reusable workflow CI
<projet>/.github/workflows/ci.yml :
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
packages: read
jobs:
ci:
uses: mister-guiiug/dev-pwa-config/.github/workflows/pwa-ci.yml@v6
# PAS de `secrets: inherit` : ce workflow ne déclare aucun secret, et
# `GITHUB_TOKEN` lui est fourni automatiquement. Hériter enverrait TOUS les
# secrets du dépôt à un workflow qui n'en demande aucun.
with:
run-doctor: true # la checklist du parc, lue sur le dépôt et sur dist/
# À poser DÈS que `vitest.config.ts` déclare des `thresholds` : sans
# elle la CI lance `npm run test`, Vitest ne mesure aucune couverture et
# le plancher n'est jamais comparé. Exige un script `test:coverage`.
run-coverage: true
run-e2e: false # passer à true quand Playwright est en place
# e2e-grep vaut '@critical|@a11y' par défaut ; un filtre qui ne trouve
# aucun test fait ÉCHOUER le job, au lieu de le laisser vert.
# e2e-project vaut 'chromium' par défaut et accepte une liste :
# 'chromium mobile-chrome' couvre aussi le téléphone (Pixel 5), sans
# navigateur supplémentaire à installer.
Reusable workflow deploy
<projet>/.github/workflows/deploy.yml :
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
packages: read
jobs:
deploy:
uses: mister-guiiug/dev-pwa-config/.github/workflows/pwa-deploy.yml@v6
# Le workflow DÉCLARE les secrets dont il a besoin : on ne passe que
# ceux-là. `secrets: inherit` enverrait tout le trousseau du dépôt.
secrets:
FIREBASE_SERVICE_ACCOUNT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY }}
with:
use-base-path: true
pre-build-script: '' # ex: 'migrate:db' pour Supabase
Repli SPA. Après le build, le workflow copie index.html en 404.html s'il manque : GitHub Pages n'a pas de repli SPA, et sans ce fichier rafraîchir un lien profond sert sa page « File not found » — quatre apps de la famille étaient dans ce cas le 02/09/2026. Un déploiement écrit à la main obtient la même chose avec spaFallbackPlugin() de vite-pwa-base.
⚠️ Ne PAS déclarer concurrency: pages au niveau du caller. Le reusable pwa-deploy.yml déclare déjà concurrency: { group: pages, cancel-in-progress: true }. Le répéter côté caller provoque le message Canceling since a deadlock was detected for concurrency group: 'pages' between a top level workflow and 'deploy' et le job ne démarre jamais. Cette règle vaut pour toutes les paires caller / reusable qui partagent un groupe de concurrence (pages, supabase-migrate, cleanup-runs-…).
Cas avancé (migrations Supabase, règles Firebase, variables nombreuses) : les réutilisables les couvrent. Les migrations passent par pwa-supabase-migrate.yml, les règles Firebase par firebase-project et firebase-only, les variables par build-env et required-env, et le gabarit templates/github-workflows/deploy.yml est un appelant prêt à copier. Écrit à la main, un déploiement Pages se prive de required-env et du chemin de base automatique (le repli 404.html s'obtient alors par spaFallbackPlugin()), et pwa-doctor le relève (wf-deploy-maison). Un workflow maison qui garde l'installation du socle appelle la composite action setup-pwa :
- uses: mister-guiiug/dev-pwa-config/.github/actions/setup-pwa@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
Reusable workflow publish
<projet>/.github/workflows/publish.yml (pour un nouveau paquet npm) :
name: Publish
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write # requis pour npm --provenance
jobs:
publish:
uses: mister-guiiug/dev-pwa-config/.github/workflows/npm-publish.yml@v6
# PAS de `secrets: inherit` : ce workflow ne déclare aucun secret, et
# `GITHUB_TOKEN` lui est fourni automatiquement. Hériter enverrait TOUS les
# secrets du dépôt à un workflow qui n'en demande aucun.
Composite actions
| Action | Rôle |
|---|
mister-guiiug/dev-pwa-config/.github/actions/setup-pwa@v6 | Setup Node 26.13.0 + scope @mister-guiiug + npm ci (auth GitHub Packages) |
mister-guiiug/dev-pwa-config/.github/actions/supabase-migrate@v6 | Setup CLI Supabase + link + db push (déploiements custom) |
mister-guiiug/dev-pwa-config/.github/actions/firebase-deploy@v6 | firebase deploy ciblé (rules database/firestore, indexes) ; auth service-account-key (recommandé) ou token (déprécié), firebase-tools épinglé via npx |
Templates non-importables (à copier-coller)
Le dossier templates/ contient des fichiers que les outils
(VSCode, husky, etc.) ne savent pas importer depuis un paquet npm. La
composition d'une app (CI, index.html, Vite) vient du squelette
pwa-starter-kit, pas d'ici — voir templates/README.md.
Renovate — hébergé par le socle
Le 02/09/2026, aucun des dix-huit dépôts n'avait jamais reçu une PR de
Renovate : treize renovate.json étendaient un préréglage dans un dépôt
.github qui n'existe pas, et l'application Mend n'était pas installée. Depuis,
tout vit ici :
| Fichier | Rôle |
|---|
renovate/default.json | Le PRÉRÉGLAGE : config:recommended, tableau de bord, samedi avant 7 h (Paris), mineures et patchs groupés sauf les ruptures, actions groupées, le socle dans une PR à part, sans attendre ; jamais engines |
renovate/self-hosted.json | QUELS dépôts : tous ceux du compte qui portent un renovate.json, sauf miss-ticket ; mister-family-map compris |
.github/workflows/renovate.yml | QUAND : le samedi 04:00 UTC (dans la fenêtre du préréglage), ou à la main avec dry-run. Muet sans le secret |
Une app étend le préréglage en une ligne :
{ "extends": ["github>mister-guiiug/dev-pwa-config//renovate/default.json"] }
Ce qu'il faut UNE fois, au propriétaire : un secret RENOVATE_TOKEN sur ce
dépôt — jeton classique avec repo, workflow et read:packages (le socle est
sur GitHub Packages). Si l'application Mend est installée un jour, désactiver
le workflow : les deux ne doivent pas tourner ensemble.
Nettoyage de l'historique Actions
Le réutilisable cleanup-runs.yml ne
conserve que les N runs les plus récents par workflow (défaut 3, option
dry-run). L'appeler en @v6 depuis un caller workflow_dispatch d'une
dizaine de lignes (exemple en tête du fichier), à permissions: actions: write :
c'est ce que recommande pwa-doctor (wf-cleanup). Le gabarit autonome
templates/github-workflows/cleanup-runs.yml
reste pour un dépôt hors famille ; il interpole encore ses entrées dans le
script, ce que le réutilisable ne fait plus.
Inputs notables des reusables
pwa-ci.yml — run-doctor (pwa-doctor après le build ; opt-in en
4.x et toujours en 6.x, défaut false) et doctor-strict ; run-npm-audit
(actif par défaut : npm audit --omit=dev à la racine et dans server-dir,
seuil npm-audit-level, défaut high) ; verify-lockfile (actif par
défaut) ; run-coverage (joue npm run test:coverage au
lieu de npm run test, pour que les thresholds de vitest.config.ts
soient vérifiés — sans quoi Vitest ne mesure rien, donc ne compare rien,
et le job sort vert ; exige le script, dont l'absence fait échouer le job) ;
e2e-grep (défaut @critical|@a11y, et un
filtre sans test fait échouer le job) ; e2e-project (défaut chromium,
mais accepte une liste : chromium mobile-chrome joue le bureau et le
téléphone, et n'installe qu'un navigateur — Pixel 5 tourne sur le chromium
déjà là) avec e2e-install pour les projets maison dont le nom n'est pas
celui d'un navigateur ; build-env (variables KEY=VALUE, une par ligne,
injectées avant build/test pour les apps Firebase/Supabase) ; server-dir
(install + tsc --noEmit + npm audit d'un backend annexe — son
package.json est un second manifeste, que l'audit de la racine ne voit pas :
au 14/09/2026 les vingt-cinq alertes restantes du parc y vivaient TOUTES, et
la racine rendait « 0 vulnérabilité » sans se tromper. Le dossier doit porter
son lockfile).
pwa-lighthouse.yml — build-env (même usage) → Lighthouse activable sur
les apps à secrets ; public-report (défaut false) pour publier en plus le
rapport sur le stockage public temporaire de Lighthouse CI. Par défaut le
rapport n'est pas publié : il reste joint en artefact du run.
pwa-deploy.yml — build-env ; déploiement Firebase optionnel
(firebase-project, firebase-only, secret FIREBASE_SERVICE_ACCOUNT_KEY)
avec auth intégrée.
Supabase keep-alive (anti-pause Free)
Le plan Free de Supabase met un projet en pause après 7 jours sans vraie
requête DB. Le reusable
pwa-supabase-keepalive.yml fait
un SELECT REST (anon key) sur une petite table keep_alive → requête réelle →
compteur d'inactivité réinitialisé.
Mise en place (un caller par projet Supabase) :
- Appliquer
templates/supabase/keep-alive.sql
au projet (SQL editor ou migration) — crée public.keep_alive + policy anon.
- Copier
templates/github-workflows/supabase-keepalive.yml
dans <projet>/.github/workflows/ (décaler le cron entre dépôts).
- Deux valeurs publiques,
VITE_SUPABASE_URL et VITE_SUPABASE_ANON_KEY
(anon = publique, jamais la service_role) : les ranger en vars, comme
toute VITE_*, et les passer dans les emplacements secrets: du
réutilisable, qui acceptent n'importe quelle expression
(VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}). Le gabarit les lit
encore dans secrets : y écrire vars, sans quoi pwa-doctor relève
vite-en-secret.
L'étape 1 conditionne les deux autres, et c'est le piège. Un caller posé
sans la table répond HTTP 404 : le ping échoue, le projet continue de
s'endormir, et rien ne distingue ce cas d'une panne réseau. Le 02/09/2026, deux
dépôts avaient le caller et les secrets sans la table — le garde-fou ne gardait
rien. Vérifier après la mise en place, sans attendre le cron :
gh workflow run supabase-keepalive.yml --repo <owner>/<app>
Le run doit finir vert avec Supabase keep-alive OK (SELECT keep_alive → 200).
Note : un cron GitHub est désactivé après 60 j sans commit sur le dépôt (les
commits Renovate suffisent ; sinon relancer via workflow_dispatch).
Personnalisation par projet
Chaque projet peut surcharger des options après extension :
- mister-puzzle ajoute
verbatimModuleSyntax + erasableSyntaxOnly (TS plus strict sur le code legacy converti) sur tsconfig.app ET tsconfig.node. Depuis le durcissement de tsconfig-node (v2.1), les options de linting (allowImportingTsExtensions, moduleDetection: force, isolatedModules, noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch) sont portées par la base : l'override projet peut être réduit au seul erasableSyntaxOnly (verbatimModuleSyntax est dans la base depuis 3.0.0).
- mister-cim10 override
allowJs: true + checkJs: false (le code legacy ICD-10 utilise du JS dans des manipulations DOM ; à durcir progressivement).
- mister-puzzle étend
vitest-base.include pour ajouter server/**/*.test.ts.
- miss-contraction étend
vitest-base avec un exclude: ['**/node_modules/**', '**/e2e/**'] pour éviter que Vitest pioche dans les specs Playwright.
Publication (changesets)
Le versioning suit changesets. Flux type :
# 1. Décrire le changement (crée .changeset/*.md ; choisir patch | minor | major)
npm run changeset
# 2. Appliquer : bump package.json + met à jour CHANGELOG.md + consomme les changesets
npm run version-packages
# 3. main est protégé : le commit de version passe par une PR, fusionnée en --merge (pas --squash)
git switch -c chore/release-x.y.z && git commit -am "chore(release): x.y.z"
git push -u origin chore/release-x.y.z && gh pr create
# 4. Publier depuis main : publish.yml lit la version et pose le tag lui-même
gh workflow run publish.yml --ref main
publish.yml publie alors sur npm.pkg.github.com
avec --provenance, avance le tag majeur mobile de la version publiée (v6
aujourd'hui ; le tag d'un majeur précédent est figé et ne reçoit plus de
correctif) et crée la GitHub Release (notes = section correspondante du
CHANGELOG.md). Il se déclenche aussi sur un tag v* poussé, à condition que
le commit de version soit déjà sur main. Versions publiées :
https://github.com/mister-guiiug/dev-pwa-config/packages
Maintenance
Toute modification de stack famille (bump majeur React, ESLint, etc.) :
- Mettre à jour les fichiers de config concernés + la « Stack cible » de ce README.
npm run changeset (choisir patch/minor/major selon l'impact consommateur).
npm run version-packages, PR fusionnée en --merge, puis gh workflow run publish.yml --ref main (cf. ci-dessus).
- Aligner les consommateurs :
node scripts/migrate-consumers.mjs <version> --write
(dry-run par défaut sans --write), puis tester chaque app.
Gouvernance
| |
|---|
CONTRIBUTING.md | Comment contribuer, et les quatre règles du dépôt — dont « promouvoir sans migrer, c'est ne pas avoir fini » |
SECURITY.md | Signalement privé d'une vulnérabilité, périmètre, et les deux limites connues qui ne sont pas des failles |
.github/CODEOWNERS | workflows/, actions/ et scripts/ demandent une relecture : ils s'exécutent dans dix-neuf dépôts |
npm run validate | Ce que la CI exécute : format, lint, types, tests |
node scripts/apply-rulesets.mjs --dry-run | Protection de main sur tous les dépôts publics non archivés, lus sur le compte ; checks exigés par dépôt |
Secrets. Chaque workflow réutilisable déclare les secrets dont il a
besoin ; un caller ne passe que ceux-là. secrets: inherit enverrait tout le
trousseau du dépôt à un workflow qui n'en demande souvent aucun — c'est le
chemin d'escalade le plus court de la famille, et il ne figure plus nulle part
dans la documentation.