Générateur d'Application PWA
Transformez une idée de projet en une application réelle dès le premier jour : elle s'exécute localement, s'installe sur un téléphone, fonctionne hors ligne, envoie des notifications et peut être partagée via un lien public. La pile ci-dessous est délibérément fixe — c'est la seule combinaison qui survive aux contraintes de l'hôte sur lequel ces projets s'exécutent (pas de compilateur, ~300 Mo de RAM, pas de PostgreSQL/MongoDB, pas de sudo). Ce qui change d'un projet à l'autre est le domaine ; ce qui reste est le squelette.
Lisez ce fichier en entier avant de faire quoi que ce soit. Ensuite, suivez les phases dans l'ordre — elles ont des portes.
0. La pile (non négociable)
| Couche | Choix | Pourquoi celui-ci et pas l'alternative évidente |
|---|---|---|
| Backend | Express 4, CommonJS, 'use strict' | Un seul processus sert l'API + l'interface construite + les images → pas de CORS, une seule chose à redémarrer, la mémoire la plus faible |
| Base de données | node:sqlite (DatabaseSync) | Intégré dans Node ≥22.5. Zéro modules natifs. better-sqlite3 ne peut même pas s'installer (pas de gcc) |
| Mots de passe | crypto.scrypt + timingSafeEqual | Pas de bcrypt (natif). Ne jamais stocker en texte brut |
| Sessions | Jeton signé HMAC dans un cookie HttpOnly | Pas de dépendance jsonwebtoken, pas d'exposition localStorage aux XSS |
| Frontend | React 18 + Vite 5 + Tailwind 3, construit dans server/public | Construction rapide à faible mémoire, sortie statique servie par Express, pas besoin de SSR |
| PWA | sw.js manuscrit + manifest.webmanifest | Pas de Workbox / vite-plugin-pwa ; contrôle total de la politique de cache |
| Push | web-push + clés VAPID générées localement | Gratuit, pas de compte Firebase |
| Images | compressées dans le navigateur avec Canvas avant le téléchargement | Pas de sharp/multer (natif), moins de RAM serveur |
| i18n | petit dictionnaire maison, pas de i18next | Commutation instantanée RTL/LTR, bundle minuscule |
N'ajoutez jamais : better-sqlite3, mongodb, pg, bcrypt, argon2, jsonwebtoken, sharp,
canvas, multer, i18next, Workbox, vite-plugin-pwa, socket.io, ou tout package avec
code natif. Si une tâche semble en nécessiter un, résolvez-la avec la bibliothèque standard ou dans le navigateur.
1. Phase 0 — Reconnaissance de l'hôte (faites ceci d'abord)
bash <skill>/scripts/check-host.sh # node, compilateur, RAM, swap, CPUs, ports, cloudflared
Cela affiche un verdict. Deux choses que vous devez intégrer dans votre plan :
- Pas de compilateur → la règle JS uniquement ci-dessus est un fait dur, pas une préférence.
- Faibles ressources (
⚠️ FAIBLES RESSOURCES) →npm installetnpm run builddoivent s'exécuter en arrière-plan avec un fichier journal que vous interrogez ; une commande longue au premier plan se fait tuer. Voirreferences/host-constraints.md.
Règle du navigateur (importante, et elle est conditionnelle)
Par défaut, vérifiez avec curl et le test de fumée de l'API — c'est plus rapide et plus économique. Mais la
règle est "ne lancez pas un navigateur sans demander la permission", pas "n'en lancez jamais un" :
-
Sur un hôte confortable (≥4 CPUs, swap présent, beaucoup de RAM libre) : demander est une formalité. Dites que vous souhaitez ouvrir un navigateur sans tête pour vérifier l'interface rendue et laissez l'utilisateur décider.
-
Sur un hôte à faibles ressources (comme 2 vCPU / ~300 Mo / pas de swap) : dites à l'utilisateur pourquoi dès le départ, dans la même phrase que la demande. Une instance Chromium peut consommer toute la machine, faire tuer le serveur par OOM et nécessiter un redémarrage. Quelque chose comme :
«تحتاج للتأكد البصري من الواجهة؟ تشغيل المتصفح على هذا السيرفر (٢ كور وبدون swap) قد يستهلك الموارد بالكامل ويوقف السيرفر فيحتاج إعادة تشغيل. أفحص بـ
curlواختبارات الـAPI بدلاً منه، أم تفضّل أن أشغّل المتصفح؟»
Respectez ensuite la réponse. Ne lancez jamais un navigateur en silence.
2. Phase 1 — Comprendre l'idée, puis interroger l'utilisateur (porte dure)
2a. Reformulez ce que vous avez compris — en 5 lignes, pas 50
Couvrez : qui l'utilise, la boucle principale (l'action qu'ils répètent le plus), les données qu'ils stockent, et à quoi ressemble le "fini" pour une première version. Ne planifiez pas encore l'architecture.
2b. Demandez des préférences avant de construire — c'est obligatoire, pas facultatif
L'utilisateur sait ce qu'il veut voir bien avant de savoir ce qu'est un service worker. Obtenir
ces réponses à l'avance est la différence entre une application qu'ils adorent du premier coup et trois
tours de "changez les couleurs". Utilisez l'outil system_ask_user pour que l'utilisateur puisse répondre d'une simple
touche — au maximum 5 questions, chacune avec exactement 3 options. Choisissez celles qui changent le plus la
construction ; sautez ce que vous pouvez raisonnablement déduire ou décider vous-même.
Banque de questions (adaptez le libellé au domaine du projet ; demandez dans la langue de l'utilisateur) :
| # | Question | Options |
|---|---|---|
| 1 | Langue d'interface par défaut | Arabe (RTL) avec bascule EN · Anglais (LTR) avec bascule AR · Arabe uniquement |
| 2 | Identité colorimétrique | Chaude (orange/ambre) · Calme (turquoise/bleu) · Élégante (violet/indigo) — ou selon le domaine du projet |
| 3 | Ambiance de conception | Simple, fonctionnel et dense en informations · Moderne avec coins arrondis et animations · Sombre élégant par défaut |
| 4 | Disposition mobile d'abord ou bureau | Application mobile avec barre inférieure · Tableau de bord bureau avec barre latérale · Les deux ensemble (réactif par défaut) |
| 5 | Comptes | Inscription + mode invité sans inscription · Invité uniquement (mise à niveau ultérieure) · Pas de comptes du tout |
| 6 | Notifications | Alertes planifiées + Push · Alertes dans l'application uniquement · Pas de notifications |
| 7 | Contenu public prêt | Catalogue de contenu alimenté par des fichiers JSON · Contenu utilisateur uniquement · Plus tard |
| 8 | Nom et logo | Nom arabe + anglais avec emoji · Un seul nom avec icône dessinée · Choisissez vous-même le meilleur |
Règles pour l'entretien : ne demandez jamais plus de 5 ; ne demandez jamais quelque chose que vous pouvez décider avec du bon goût (espacement, courbe d'animation) ; si l'utilisateur dit «اختر أنت», décidez et passez à autre chose — ne redemandez pas. Enregistrez les réponses : elles deviennent les indicateurs d'échafaudage dans la Phase 3.
2c. Répondez au plan et attendez
Donnez un plan court (pile déjà décidée ; écrans ; modèle de données en une ligne ; les phases) et arrêtez-vous. La plupart des utilisateurs dans ce flux de travail veulent approuver avant que le code soit écrit — une ligne «موافق، ابدأ» vaut plus qu'une construction rapide et erronée.
3. Phase 2 — Concevoir le domaine (petit, concret)
Avant d'écrire du code, notez dans le chat :
- Entités + champs — chaque champ dont la boucle principale de l'utilisateur a besoin ; marquez requis vs facultatif. Gardez les champs requis au strict minimum (un nom et une catégorie valent mieux qu'un formulaire à dix champs).
- Écrans — listez-les ; chaque écran correspond à un fichier dans
web/src/pages/. - Routes — un groupe REST par entité plus l'authentification.
- Quels modules facultatifs s'appliquent — comptes/invité, alarmes+push, liens de partage public, catalogue de contenu public. Chacun est déjà implémenté dans les modèles ; vous choisissez, vous n'inventez pas.
4. Phase 3 — Échafaudage (une commande, puis vérifiez)
node <skill>/scripts/scaffold.mjs \
--dir <workspace> --name "اسم التطبيق" --name-en "AppName" \
--slug appname --prefix ap --palette orange --ink warm [--with-catalog]
Cela plante ~68 fichiers : le squelette complet et fonctionnel (auth, une entité CRUD de démonstration, shell PWA, push,
i18n, kit UI, scripts d'exploitation) avec l'identité de l'application, le préfixe de cookie et la palette de marque
déjà substitués partout. Palettes : orange amber rose violet indigo sky teal emerald;
encres : warm cool stone.
Ensuite, avant de toucher à quoi que ce soit d'autre — prouvez que le pipeline fonctionne :
export PATH="$HOME/node-v24.20.0-linux-x64/bin:$PATH" # Node n'est pas dans le PATH sur ces hôtes
npm install # hôte à faible RAM ? exécutez en arrière-plan avec un journal et interrogez-le
npm run build
./scripts/run.sh start
curl -s http://127.0.0.1:3000/api/health
npm run test:api # attend "110 ناجح، 2 فاشل" sur un échafaudage de démonstration frais
Les 2 échecs sont les assertions de catalogue qui nécessitent de vrais fichiers image
(npm run catalog:images les récupère ; nécessite un réseau). Tout le reste doit être vert avant
de commencer à changer le domaine — sinon vous ne saurez pas si un échec ultérieur est le vôtre.
Lisez references/templates-guide.md pour savoir ce qu'est chaque fichier planté et quelles parties sont le
domaine de démonstration que vous êtes sur le point de remplacer.
5. Phase 4 — Remplacer le domaine de démonstration par le vrai
L'échafaudage livre un exemple de travail complet (une application de repas) précisément pour que vous puissiez copier sa forme. Travaillez vers l'extérieur à partir de la couche de données pour que rien ne pende :
server/db.js— réécrivez les tables de démonstration pour vos entités. Toujours additif : nouvelles tables dans le blocCREATE TABLE IF NOT EXISTS, nouvelles colonnes ajoutées au tableau de migration. Ensuite, écrivez les fonctions de requête.server/routes/<entity>.js— copiezroutes/meals.jscomme routeur CRUD canonique (optionalFields, un mappeurtoEntity(), INSERT/PATCH). Faites correspondre le style d'exportation à la façon dontserver/index.jsles importe (module.exports = routervs{ router }— le dépôt est incohérent, vérifiez avant d'écrire).- Montez le routeur dans
server/index.js; n'ajoutez le préfixe API à rien d'autre (le fallback SPA exclut déjà/api). web/src/lib/api.js— une méthode par route. Pas defetchdans les pages ou les composants, jamais.web/src/pages/*+ routes dansweb/src/App.jsx.scripts/smoke-test.js— réécrivez les assertions de démonstration en utilisant les assistants existantscheck(name, condition, extra)+call(path, {method, body}). C'est votre preuve.
Supprimez ce dont votre domaine n'a pas besoin (le routeur, la page, les méthodes API et les clés i18n de l'entité de démonstration) plutôt que de laisser du code mort.
6. Phase 5 — Frontend : RTL d'abord, les deux langues, un seul système de conception
- Chaque chaîne destinée à l'utilisateur va dans les deux
dict.aretdict.endansweb/src/lib/i18n.js, lue avect('key'), interpolation via les espaces réservés{name}. Une clé dans un seul dictionnaire est un bogue. - Utilisez des utilitaires logiques (
start-*/end-*/ps-*/pe-*/ms-*/me-*) pour que RTL et LTR fonctionnent tous les deux. L'exception délibérée : les champs de mot de passe sontdir="ltr"et utilisent donc les valeurs physiquespr-12/right-3. - Réutilisez les classes de composants dans
web/src/styles.css(.card,.btn-primary,.input,.label,.chip) et les primitives dansweb/src/components/ui.jsxavant d'écrire du Tailwind dispersé. - Le thème est basé sur les classes sur
documentElement, persistant danslocalStorage, et appliqué par un script en ligne dansweb/index.htmlavant le premier affichage (tue le flash et corrige la barre d'état iOS). - Réactif : mobile-first avec une navigation en bas qui devient un rail latéral à partir de
md:(AppShell.jsx). - Vérifiez RTL/LTR sur : formulaires, listes, barre d'application, modales et position des toasts.
7. Phase 6 — PWA + notifications
sw.js et manifest.webmanifest sont plantés. Deux choses à ne pas oublier :
- Incrémentez
VERSIONdansweb/public/sw.jschaque fois que vous modifiez ce fichier, ou les clients installés garderont le cache obsolète pour toujours. - Les clés VAPID sont générées au premier démarrage dans
data/vapid.json. LesubVAPID doit être un vrai domaine résoluble (mailto:[email protected]) — Apple rejette*.local/@localhostavec403 BadJwtTokentandis que Google l'accepte, ce qui produit le classique "fonctionne sur Android, échoue silencieusement sur iPhone". Remplacez avecVAPID_SUBJECT; ne codez jamais en dur une adresse personnelle. - Ne signalez jamais le succès d'un push sans la réponse réelle du fournisseur :
sendToUserretourne{total, sent, failed:[{status, reason}]}et élimine les abonnements morts (404/410/403). - iOS nécessite que l'application soit installée sur l'écran d'accueil et que la permission soit demandée à partir d'une
tape. Gardez le fallback de notification locale dans l'application (
useAlarmWatcherdansApp.jsx) fonctionnel.
Les détails et les limites de la plateforme (images dans les notifications, couleur de la barre d'état,
letterboxing autonome) se trouvent dans references/pwa-and-push.md — lisez-le avant de promettre quoi que ce soit sur iOS.
8. Phase 7 — Vérifier (c'est la preuve de la livraison)
npm run test:api # données isolées test/data.db sur le port 3100, base de données live intacte
npm run build && ./scripts/run.sh restart
curl -s http://127.0.0.1:3000/api/health
curl -s http://127.0.0.1:3000/ | head -3 # la SPA construite est servie
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/manifest.webmanifest
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/sw.js
Exercez également le chemin utilisateur réel avec curl (inscription/invité → créer → lire → mettre à jour → supprimer) pour
pouvoir signaler une sortie réelle, pas "devrait fonctionner". Ensuite, si un navigateur est autorisé, utilisez-le pour la vérification
visuelle sur une fenêtre de taille téléphone.
Signalez les commandes exactes et leur sortie réelle. Si quelque chose a échoué, dites-le avec l'erreur.
9. Phase 8 — Livrer
- Dépôt git local + premier commit (
git init -b main), pour avoir un état de référence connu auquel revenir avant toute expérience risquée. Ne jamaispushsauf demande. Commits conventionnels avec un sujet arabe :feat(meals): إضافة فلتر الفئات. - Lien public via le tunnel rapide Cloudflare (
./scripts/tunnel.sh start, puis lisez l'URL depuistunnel.log). Cette étape est facultative et nécessite deux approbations distinctes :- Demandez s'ils veulent un lien public du tout. S'ils possèdent déjà un domaine, un proxy inverse ou un hébergement sur lequel ils peuvent déployer, ils n'ont pas besoin de tunnel — n'en poussez pas un.
- Vérifiez
cloudflaredavant de promettre quoi que ce soit (scripts/check-host.shle signale). S'il est manquant, dites-le et demandez la permission de l'installer avant d'exécuter un installateur ; n'installez jamais un binaire en silence. Proposez `brew
…
[tronqué pour la traduction]