@nodefony/devkit 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +318 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +47 -0
- package/dist/nodefony/command/CardCommand.js +70 -0
- package/dist/nodefony/config/config.js +200 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controllers/DevkitController.js +60 -0
- package/dist/nodefony/controllers/McpController.js +223 -0
- package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
- package/dist/nodefony/interfaces/IDevkitService.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DevkitService.js +198 -0
- package/dist/nodefony/src/card.js +2 -0
- package/dist/nodefony/src/errors/DevkitError.js +21 -0
- package/dist/nodefony/src/mcp/guard.js +51 -0
- package/dist/nodefony/src/mcp/protocol.js +127 -0
- package/dist/nodefony/src/mcp/server.js +133 -0
- package/dist/nodefony/src/mcp/tools.js +163 -0
- package/dist/types/index.d.ts +52 -0
- package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
- package/dist/types/nodefony/config/config.d.ts +25 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
- package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
- package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
- package/dist/types/nodefony/src/card.d.ts +18 -0
- package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
- package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
- package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
- package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
- package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
- package/docs/index.md +358 -0
- package/package.json +77 -0
- package/skills/nodefony-add-crud/SKILL.md +199 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
- package/skills/nodefony-add-service/SKILL.md +90 -0
- package/skills/nodefony-browser/SKILL.md +416 -0
- package/skills/nodefony-browser/references/socket.md +115 -0
- package/skills/nodefony-browser/references/sondes.md +175 -0
- package/skills/nodefony-browser/scripts/audit.mjs +169 -0
- package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
- package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
- package/skills/nodefony-browser/scripts/socket.mjs +354 -0
- package/skills/nodefony-browser/scripts/watch.mjs +125 -0
- package/skills/nodefony-migrate-schema/SKILL.md +359 -0
- package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
- package/skills/nodefony-protect-route/SKILL.md +195 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Le socket, de bout en bout — `socket.mjs` et le protocole qu'il parle
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante, jamais un journal. Éditer en place ; l'historique vit dans git.
|
|
4
|
+
|
|
5
|
+
`socket.mjs` pilote un endpoint temps réel Nodefony COMPLET, depuis une vraie page : accueil,
|
|
6
|
+
abonnement, action, latence, pont API, reconnexion. Le scénario s'exécute **dans la page** — la
|
|
7
|
+
connexion porte donc les cookies de session et l'`Origin` réels. Un client « à côté » n'aurait ni
|
|
8
|
+
l'un ni l'autre, et l'on croirait à un refus d'authentification là où il n'y a qu'un décor faux.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
docker exec -e NF_BROWSER_API=/api/sante mon-app-browser node /app/see-screen/socket.mjs /chat/realtime
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Le protocole du fil — quatre formes de frame, à l'œil
|
|
15
|
+
|
|
16
|
+
Tout ce qui passe est du **JSON-RPC 2.0**. La nature d'une frame se lit sur `method`, jamais sur
|
|
17
|
+
`id` :
|
|
18
|
+
|
|
19
|
+
| Forme | Contenu | Exemple |
|
|
20
|
+
| ----------------- | ------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
21
|
+
| **Notification** | `method` seul — aucune réponse, jamais | `{"jsonrpc":"2.0","method":"subscribe","params":{"channel":"orders:new"}}` |
|
|
22
|
+
| **Requête** | `method` + `id` — une réponse est due | `{"jsonrpc":"2.0","id":1,"method":"api.request","params":{"path":"/api/sante"}}` |
|
|
23
|
+
| **Réponse** | `id` seul, `result` OU `error` | `{"jsonrpc":"2.0","id":1,"result":{…},"meta":{"requestId":"…"}}` |
|
|
24
|
+
| **Push de canal** | notification dont le NOM du canal est la `method` | `{"jsonrpc":"2.0","method":"orders:new","params":{…}}` |
|
|
25
|
+
|
|
26
|
+
Deux frames servent de repères :
|
|
27
|
+
|
|
28
|
+
- **`realtime:welcome`** — la première notification poussée par le serveur : protocole, **canaux**
|
|
29
|
+
annoncés, **actions** exposées, **identité résolue au handshake**. C'est la carte du territoire ;
|
|
30
|
+
tant qu'elle n'est pas là, rien d'autre n'a de sens.
|
|
31
|
+
- **`realtime:denied`** — le refus d'une notification. Une notification n'a pas de canal de
|
|
32
|
+
réponse : sans cette frame dédiée, un abonnement refusé serait indiscernable d'un canal
|
|
33
|
+
silencieux. La sonde l'écoute, et son verdict `REFUSÉ` vient de là.
|
|
34
|
+
|
|
35
|
+
## Les étapes du scénario, et comment lire chaque verdict
|
|
36
|
+
|
|
37
|
+
### `welcome`
|
|
38
|
+
|
|
39
|
+
`receivedAfterMs`, canaux, méthodes, identité. **S'il ne vient jamais** (code de retour 65), les trois
|
|
40
|
+
suspects, dans l'ordre : le chemin du endpoint est faux ; la page n'est pas authentifiée (donner
|
|
41
|
+
`NF_BROWSER_USER` + `NF_BROWSER_LOGIN`) ; l'`Origin` de la page est refusé par le serveur. Le
|
|
42
|
+
réseau qui « passe » n'innocente aucun des trois.
|
|
43
|
+
|
|
44
|
+
### `subscription`
|
|
45
|
+
|
|
46
|
+
S'abonne au canal de `NF_BROWSER_CHANNEL` — à défaut, au **premier canal annoncé par l'accueil** —
|
|
47
|
+
puis écoute pendant `NF_BROWSER_SOCKET_WAIT` ms (défaut 4 000).
|
|
48
|
+
|
|
49
|
+
- `OK` : au moins une poussée reçue (horodatées, tronquées, plafonnées).
|
|
50
|
+
- `REFUSÉ` : le serveur a poussé `realtime:denied` pour ce canal — droits insuffisants ou plafond
|
|
51
|
+
de canaux. Le motif reste générique par conception : le serveur ne dit jamais QUEL droit manquait.
|
|
52
|
+
- `SILENCIEUX` : **pas forcément une panne.** Un canal d'événements ne pousse que quand il se passe
|
|
53
|
+
quelque chose ; seul un canal d'état cadencé garantit du trafic dans la fenêtre. Avant de
|
|
54
|
+
conclure, provoquer un événement, allonger la fenêtre, ou choisir un canal cadencé.
|
|
55
|
+
|
|
56
|
+
L'abonnement part **sans `id`** : c'est une notification. Envoyé avec un `id`, il serait classé
|
|
57
|
+
requête, ne trouverait aucun handler, et récolterait un `-32601` — piège classique du protocole.
|
|
58
|
+
|
|
59
|
+
### `latency`
|
|
60
|
+
|
|
61
|
+
Mesure `NF_BROWSER_PINGS` allers-retours (défaut 5) sur une méthode **corrélée** : l'action de
|
|
62
|
+
`NF_BROWSER_ACTION` si elle est donnée, sinon le pont API si `NF_BROWSER_API` l'est. Rend chaque
|
|
63
|
+
mesure, les expirations, et la **médiane** — jamais la moyenne, qu'un seul aller-retour aberrant
|
|
64
|
+
(ramasse-miettes, réveil de connexion) suffit à déplacer.
|
|
65
|
+
|
|
66
|
+
- **Sans méthode corrélée, pas de latence** : la notification `ping` du battement de cœur est un
|
|
67
|
+
no-op serveur, aucun pong n'en revient. Verdict `NON MESURÉE`, jamais un zéro inventé.
|
|
68
|
+
- **`RÉPOND EN ERREUR -32601`** : l'aller-retour est COMPLET — la latence mesure le fil, mais ne
|
|
69
|
+
valide pas l'action, qui n'existe pas sur cet endpoint. Lire `methods` dans l'accueil avant
|
|
70
|
+
d'appeler.
|
|
71
|
+
|
|
72
|
+
### `api`
|
|
73
|
+
|
|
74
|
+
Rejoue une route HTTP de l'application **sur le socket** (`api.request`, `params.path`). La réponse
|
|
75
|
+
porte le `result` de la route et, souvent, un champ frère `meta` (identifiant de requête serveur) —
|
|
76
|
+
la preuve que le plan de données passe bien par le WebSocket. Une erreur corrélée (`-32601` si le
|
|
77
|
+
pont n'est pas exposé sur cet endpoint, erreur applicative sinon) est rendue telle quelle.
|
|
78
|
+
|
|
79
|
+
### `reconnection`
|
|
80
|
+
|
|
81
|
+
Ferme le socket (code 1000), en rouvre un, attend le nouvel accueil, et compare l'identité.
|
|
82
|
+
`memeIdentite: true` prouve que l'identité est portée par la **session** (résolue au handshake,
|
|
83
|
+
jamais dans les frames) : c'est la propriété qui compte pour une application qui reconnecte en
|
|
84
|
+
production. Un `ÉCHEC` ici avec un premier accueil réussi désigne un serveur qui refuse la
|
|
85
|
+
DEUXIÈME connexion — plafond de connexions, ou état serveur consommé par la première.
|
|
86
|
+
|
|
87
|
+
## Variables d'environnement
|
|
88
|
+
|
|
89
|
+
<!-- prettier-ignore -->
|
|
90
|
+
| Variable | Rôle | Défaut |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| `NF_BROWSER_SOCKET` | chemin du endpoint (ou 1er argument) — **requis**, rien n'est deviné | — |
|
|
93
|
+
| `NF_BROWSER_PAGE` | page ouverte AVANT le socket — elle porte cookies et `Origin` | `/` |
|
|
94
|
+
| `NF_BROWSER_CHANNEL` | canal à écouter | 1er canal annoncé |
|
|
95
|
+
| `NF_BROWSER_ACTION` | action RPC à appeler (la latence la réutilise) | aucune |
|
|
96
|
+
| `NF_BROWSER_ACTION_PARAMS` | paramètres JSON de l'action | aucun |
|
|
97
|
+
| `NF_BROWSER_API` | chemin rejoué par le pont `api.request` | aucun |
|
|
98
|
+
| `NF_BROWSER_SOCKET_WAIT` | fenêtre d'écoute du canal (ms) | 4000 |
|
|
99
|
+
| `NF_BROWSER_PINGS` | nombre de mesures de latence | 5 |
|
|
100
|
+
|
|
101
|
+
Plus les variables communes à toutes les sondes : `NF_BROWSER_BASE`, `NF_BROWSER_LOGIN`,
|
|
102
|
+
`NF_BROWSER_USER`, `NF_BROWSER_PASSWORD`.
|
|
103
|
+
|
|
104
|
+
## Quand cette sonde se trompe
|
|
105
|
+
|
|
106
|
+
- **Elle parle le protocole à la main, sans la bibliothèque cliente.** C'est voulu — on observe le
|
|
107
|
+
FIL, pas une surcouche — mais ce que la bibliothèque ferait en plus (reconnexion automatique,
|
|
108
|
+
ré-abonnements, cadence adaptative) n'est PAS exercé ici : la « reconnexion » du scénario prouve
|
|
109
|
+
que le serveur accepte une nouvelle connexion authentifiée, pas que le client de l'application
|
|
110
|
+
reconnecte bien.
|
|
111
|
+
- **La fenêtre d'écoute échantillonne.** Trois poussées en 4 s ne disent rien du débit de pointe ni
|
|
112
|
+
d'une fuite lente — c'est `watch.mjs`, plus longtemps, qui observe une dérive.
|
|
113
|
+
- **Une latence médiane de quelques millisecondes est celle du conteneur vers l'hôte** — un
|
|
114
|
+
aller-retour local. Elle borne le coût du protocole, elle ne prédit pas la latence d'un
|
|
115
|
+
utilisateur réel derrière un réseau.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Les sondes d'`inspect.mjs` — ce que chaque famille mesure, et quand elle se trompe
|
|
2
|
+
|
|
3
|
+
> **Maintenance** : vérité courante, jamais un journal. Éditer en place ; l'historique vit dans git.
|
|
4
|
+
|
|
5
|
+
`inspect.mjs` rend toujours un **socle**, et n'ajoute une **famille** de sondes que si on la
|
|
6
|
+
demande (`NF_BROWSER_FAMILIES=a11y,perf`, ou `toutes`). Un nom de famille inconnu est **refusé**
|
|
7
|
+
(code 64), jamais ignoré : une famille fautée en silence ferait croire qu'on a mesuré ce qu'on n'a
|
|
8
|
+
pas mesuré.
|
|
9
|
+
|
|
10
|
+
Chaque famille rend un **`verdict`** (`OK` / `ALERTE`) et des données bornées (comptes + 3
|
|
11
|
+
exemples, jamais l'inventaire). Le champ `verdict` de fin de sortie agrège les familles actives —
|
|
12
|
+
`OK` seulement si tout est OK. **Le code de retour reste 0** : le verdict est une donnée, pas une
|
|
13
|
+
panne de la sonde. Les codes non nuls disent autre chose — 64 : usage (famille inconnue,
|
|
14
|
+
identifiant sans chemin de connexion) ; 65 : le texte attendu n'est jamais apparu.
|
|
15
|
+
|
|
16
|
+
## Le socle — toujours rendu
|
|
17
|
+
|
|
18
|
+
<!-- prettier-ignore -->
|
|
19
|
+
| Champ | Ce que c'est |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `url` | La page RÉELLEMENT ouverte — à comparer à celle demandée (redirection de connexion, 404 SPA). |
|
|
22
|
+
| `theme` / `lang` | `color-scheme` **calculé** (ce que le moteur applique) et attribut `lang` de la racine. |
|
|
23
|
+
| `title` | `document.title`. |
|
|
24
|
+
| `scripts` | Les scripts RÉELLEMENT servis — pour vérifier qu'on observe bien le bundle qu'on vient de bâtir. |
|
|
25
|
+
| `probes` | Les sondes de style (voir ci-dessous). |
|
|
26
|
+
| `violationsCSP` | Les violations de Content-Security-Policy vues PAR la page — le réseau montre l'absence, jamais la raison. |
|
|
27
|
+
| `consoleErrors` | Les `console.error` émis pendant la mesure. |
|
|
28
|
+
| `uncaughtErrors` | Les exceptions non capturées (`pageerror`) — elles ne passent pas toutes par la console. |
|
|
29
|
+
| `capture` | Le PNG horodaté déposé dans le volume monté. |
|
|
30
|
+
|
|
31
|
+
Les erreurs de console et les violations CSP **ne pèsent pas** dans le verdict global : un parcours
|
|
32
|
+
de connexion produit des `401` légitimes, et les trancher ici les ferait passer pour des pannes.
|
|
33
|
+
C'est au lecteur de juger — la sonde fournit, elle ne condamne pas ce qu'elle ne peut pas qualifier.
|
|
34
|
+
|
|
35
|
+
## Les sondes de style (`probes`) — le contraste CALCULÉ
|
|
36
|
+
|
|
37
|
+
Un sélecteur par élément (`NF_BROWSER_PROBES=libellé=sélecteur,…`) ; pour chacun : texte, couleur,
|
|
38
|
+
fond effectif, rapport de contraste, police, verdict WCAG, taille rendue.
|
|
39
|
+
|
|
40
|
+
- **Le fond effectif empile TOUTES les couches** jusqu'au premier ancêtre opaque, puis les compose.
|
|
41
|
+
Deux erreurs à ne pas refaire : lire `backgroundColor` sur l'élément rend `rgba(0,0,0,0)` et un
|
|
42
|
+
contraste faux ; s'arrêter à la première couche non transparente traite un voile à 13 % comme un
|
|
43
|
+
aplat plein, c'est-à-dire comme une couleur que personne ne voit.
|
|
44
|
+
- **Les couleurs modernes ne comptent pas dans la même échelle.** `rgb(0, 87, 156)` est en 0–255,
|
|
45
|
+
`color(srgb 0 0.34 0.61 / 0.13)` en 0–1. Les lire avec la même expression régulière rend un bleu
|
|
46
|
+
presque noir — et fabrique des échecs qui noient les vrais.
|
|
47
|
+
- **Le verdict WCAG dépend de la POLICE** : 3:1 suffit à un texte « large » (≥ 24 px, ou 18,66 px
|
|
48
|
+
en gras), 4,5:1 sinon. Un contraste rendu sans sa police ne conclut rien.
|
|
49
|
+
- **Quand elle se trompe** : un fond en dégradé ou une image de fond ne sont pas vus — la sonde lit
|
|
50
|
+
des COULEURS, pas le pixel composité. Sur ces cas, juger sur la capture.
|
|
51
|
+
|
|
52
|
+
> Ces sondes visent **un** élément qu'on désigne. Pour balayer la page entière sans rien désigner,
|
|
53
|
+
> prendre la famille `axe` : elle voit ce à quoi on ne pensait pas.
|
|
54
|
+
|
|
55
|
+
## `axe` — l'audit WCAG par un moteur dont c'est le métier
|
|
56
|
+
|
|
57
|
+
Une centaine de règles jouées par `axe-core`, dont le contraste de **tout** le texte visible. C'est
|
|
58
|
+
le moteur qu'embarque Lighthouse pour son volet accessibilité.
|
|
59
|
+
|
|
60
|
+
<!-- prettier-ignore -->
|
|
61
|
+
| Champ | Ce qu'il dit |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `failures` | Les défauts AVÉRÉS, comptés par gravité (critique, sérieux, modéré, mineur) |
|
|
64
|
+
| `worst` | Jusqu'à 8 règles, du plus grave au moins grave, avec **5 cibles** chacune et le `reason` calculé (contraste mesuré, rôle attendu) |
|
|
65
|
+
| `otherTargets` | Ce qui dépasse les 5 — annoncé, jamais tronqué en silence |
|
|
66
|
+
| `toReview` | Ce que le moteur REFUSE de trancher (fond en image…) — **pas** des défauts |
|
|
67
|
+
| `passed` | Les règles passées, pour situer le reste |
|
|
68
|
+
|
|
69
|
+
- **`toReview` n'est pas un manquement** et ne déclenche pas l'alerte. Le confondre ferait crier la
|
|
70
|
+
sonde sur des pages saines, et on cesserait de la lire.
|
|
71
|
+
- **Cinq cibles par règle, pas une.** Une même règle couvre des défauts à des endroits différents,
|
|
72
|
+
qui ne se corrigent pas d'un seul geste ; n'en montrer qu'un fait croire le travail fini.
|
|
73
|
+
- **N'écris jamais ce calcul toi-même.** Mesuré en conditions réelles : une sonde maison a rendu
|
|
74
|
+
**41 faux positifs** masquant **7 défauts réels**, dont celui qu'on cherchait — à cause de trois
|
|
75
|
+
cas particuliers (échelle des couleurs modernes, alpha non composé, emoji peints par une police en
|
|
76
|
+
couleurs) qu'on ne devine pas avant de les avoir vus.
|
|
77
|
+
- **Quand elle est indisponible** : `axe-core` vit dans les dépendances du projet. En conteneur, il
|
|
78
|
+
faut le copier à part. La famille rend alors `verdict: "INDISPONIBLE"` et la commande à taper —
|
|
79
|
+
jamais un `OK` qui n'a rien mesuré.
|
|
80
|
+
|
|
81
|
+
## `a11y` — ce qu'un lecteur d'écran ou un clavier rencontrent
|
|
82
|
+
|
|
83
|
+
| Champ | Question à laquelle il répond |
|
|
84
|
+
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `lang` | La racine annonce-t-elle sa langue (sans elle, la synthèse vocale lit avec le mauvais accent) ? |
|
|
86
|
+
| `headings` | Un seul `h1` ? Des sauts de niveau (`h2→h4`) qui cassent la table des matières ? |
|
|
87
|
+
| `imagesWithoutAlt` | Des images sans attribut `alt` — muettes pour un lecteur d'écran. |
|
|
88
|
+
| `fieldsWithoutLabel` | Des champs sans étiquette (label, `aria-label`, `aria-labelledby`, `title`). |
|
|
89
|
+
| `controlsWithoutName` | Boutons et liens sans nom accessible — le bouton-icône muet, le cas réel. |
|
|
90
|
+
| `targetsTooSmall` | Cibles interactives < 24×24 px ; les liens DANS le texte sont exemptés, comme dans le critère. |
|
|
91
|
+
| `positiveTabIndexValues` | Un `tabindex` positif impose un ordre de focus manuel qui diverge du DOM — l'anti-pattern du parcours clavier. |
|
|
92
|
+
| `visibleFocusables` | L'ampleur du parcours clavier de la page. |
|
|
93
|
+
| `tree` | L'arbre d'accessibilité (rôles + noms), tel que le calcule le navigateur — tronqué : il dit la STRUCTURE, pas l'inventaire. |
|
|
94
|
+
|
|
95
|
+
**Quand elle se trompe** : le nom accessible est calculé de façon SIMPLIFIÉE (l'algorithme complet
|
|
96
|
+
de la norme fait plus) — un composant qui pose son nom par un mécanisme exotique peut être compté
|
|
97
|
+
« sans nom » à tort ; vérifier dans `tree`, qui lui applique le calcul complet du navigateur.
|
|
98
|
+
Et une sonde automatique ne couvre qu'une fraction de l'accessibilité : elle attrape le mesurable
|
|
99
|
+
(étiquettes, tailles, structure), jamais le sens — l'ordre logique d'un formulaire ou la pertinence
|
|
100
|
+
d'un `alt` restent un jugement humain.
|
|
101
|
+
|
|
102
|
+
## `rendu` — la page tient-elle dans son viewport, ses polices sont-elles là
|
|
103
|
+
|
|
104
|
+
- `horizontalOverflow` : la page dépasse-t-elle la largeur de la fenêtre (le défilement
|
|
105
|
+
horizontal accidentel). C'est LUI qui porte le verdict.
|
|
106
|
+
- `elementsOutsideViewport` : une **information**, pas un verdict — carrousels, tiroirs et textes
|
|
107
|
+
destinés aux lecteurs d'écran sortent du viewport légitimement.
|
|
108
|
+
- `fonts` : ce que `document.fonts` a RÉELLEMENT chargé (statut par famille + graisse). Une
|
|
109
|
+
police en échec bascule le verdict — le texte s'affiche alors dans une police de repli, et toutes
|
|
110
|
+
les mesures de taille en héritent.
|
|
111
|
+
|
|
112
|
+
**Quand elle se trompe** : la mesure attend `document.fonts.ready` au plus 2 s — une police servie
|
|
113
|
+
très lentement peut encore être `loading` au moment de la lecture, sans être en échec.
|
|
114
|
+
|
|
115
|
+
## `reseau` — requêtes, échecs, poids, temps
|
|
116
|
+
|
|
117
|
+
Compte par type, octets réellement transférés, échecs (statut ≥ 400 et requêtes avortées),
|
|
118
|
+
ressources **lourdes** (> `NF_BROWSER_SEUIL_LOURD`, défaut 512 000 octets) et **lentes**
|
|
119
|
+
(> `NF_BROWSER_SEUIL_LENT`, défaut 1 000 ms). Verdict : ALERTE dès un échec ou une ressource lourde.
|
|
120
|
+
|
|
121
|
+
**Quand elle se trompe** :
|
|
122
|
+
|
|
123
|
+
- En développement, un serveur d'assets qui livre les modules UN PAR UN rend des centaines de
|
|
124
|
+
requêtes et des mégaoctets non minifiés : c'est le DÉCOR du mode dev, pas une régression. Les
|
|
125
|
+
seuils jugent une application SERVIE — comparer dev et prod n'a pas de sens.
|
|
126
|
+
- Les tailles viennent du transfert réel quand le navigateur les donne, de `content-length` sinon ;
|
|
127
|
+
`unknownBytes` compte ce qui n'a pu être pesé — un total avec beaucoup d'inconnus minore.
|
|
128
|
+
- La collecte s'arrête à la mesure : ce que la page télécharge APRÈS (interaction, différé) n'est
|
|
129
|
+
pas vu — c'est le travail de `watch.mjs`.
|
|
130
|
+
|
|
131
|
+
## `perf` — temps de rendu et stabilité visuelle
|
|
132
|
+
|
|
133
|
+
`ttfbMs`, `domContentLoadedMs`, `loadCompleteMs`, `fcpMs`, `lcpMs`, `cls`, `longTasks` —
|
|
134
|
+
verdict sur les seuils « bons » des Web Vitals : LCP ≤ 2 500 ms, CLS ≤ 0,1.
|
|
135
|
+
|
|
136
|
+
**Quand elle se trompe** :
|
|
137
|
+
|
|
138
|
+
- **Une seule visite n'est pas une statistique.** Cache froid ou chaud, machine chargée, premier
|
|
139
|
+
boot d'un serveur de dev : la même page varie du simple au double. Un verdict ALERTE isolé se
|
|
140
|
+
vérifie en relançant ; une tendance se mesure en médiane de plusieurs passes.
|
|
141
|
+
- LCP et CLS sont observés PENDANT le chargement (observateurs injectés avant la navigation) : la
|
|
142
|
+
sonde ne voit pas les décalages provoqués ENSUITE par une interaction.
|
|
143
|
+
- Le CLS d'une application en mode développement (styles injectés à la volée) est structurellement
|
|
144
|
+
plus mauvais qu'en production.
|
|
145
|
+
|
|
146
|
+
## `stockage` — cookies et Web Storage
|
|
147
|
+
|
|
148
|
+
Attributs des cookies (`secure`, `httpOnly`, `sameSite`, expiration) et inventaire du
|
|
149
|
+
`localStorage`/`sessionStorage` (clés, octets, les 5 plus grosses). **Jamais les valeurs** : un
|
|
150
|
+
jeton de session imprimé dans une sortie de sonde finit dans un terminal, un log de CI, un rapport
|
|
151
|
+
— il a fuité. Verdict : ALERTE si un cookie sans `secure` circule sur une origine https.
|
|
152
|
+
|
|
153
|
+
**Quand elle se trompe** : les octets du Web Storage comptent en unités UTF-16 (×2) — c'est
|
|
154
|
+
l'empreinte mémoire, pas la taille « à l'écran » ; et un cookie `httpOnly: false` n'est pas signalé
|
|
155
|
+
comme alerte alors qu'il mérite un regard si sa valeur est sensible.
|
|
156
|
+
|
|
157
|
+
## `responsive` — la même page à plusieurs largeurs
|
|
158
|
+
|
|
159
|
+
Rejoue la mesure de débordement horizontal à chaque largeur de `NF_BROWSER_WIDTHS` (défaut
|
|
160
|
+
`360,768,1280`). Par largeur : dépassement en pixels, nombre d'éléments débordants, 3 exemples.
|
|
161
|
+
|
|
162
|
+
**Quand elle se trompe** : redimensionner un viewport n'est pas changer d'appareil — ni densité de
|
|
163
|
+
pixels, ni tactile, ni `user-agent`. Une media query sur `pointer` ou `hover` ne réagira pas. Et la
|
|
164
|
+
capture PNG est prise AVANT cette famille, à la largeur d'origine : les débordements constatés ici
|
|
165
|
+
ne s'y voient pas.
|
|
166
|
+
|
|
167
|
+
## Lire un verdict sans se faire piéger
|
|
168
|
+
|
|
169
|
+
1. **`ALERTE` n'est pas « cassé »** — c'est « mérite un regard ». Le détail dit lequel.
|
|
170
|
+
2. **`OK` n'est pas « accessible / rapide / propre »** — c'est « rien de mesurable à signaler dans
|
|
171
|
+
cette famille, sur cette page, à cet instant ».
|
|
172
|
+
3. **Une mesure en mode développement juge le mode développement.** Poids, nombre de requêtes et
|
|
173
|
+
CLS ne se comparent qu'à décor égal.
|
|
174
|
+
4. **Le verdict global agrège, il n'explique pas.** Toujours descendre dans la famille qui l'a fait
|
|
175
|
+
basculer.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Lighthouse d'une page, y compris DERRIÈRE une authentification.
|
|
3
|
+
*
|
|
4
|
+
* Pourquoi un script à part plutôt qu'une famille d'`inspect.mjs` : Lighthouse
|
|
5
|
+
* ne mesure pas une page ouverte, il en PILOTE le chargement — il recharge, vide
|
|
6
|
+
* le cache, bride le réseau et le processeur, et chronomètre. C'est l'inverse de
|
|
7
|
+
* la photographie d'un instant, et cela prend des dizaines de secondes.
|
|
8
|
+
*
|
|
9
|
+
* Comment l'authentification survit, alors que Lighthouse ouvre son propre
|
|
10
|
+
* onglet : le navigateur est lancé avec un PROFIL PERSISTANT et un port de
|
|
11
|
+
* débogage. On s'y connecte normalement — les témoins de session vivent alors
|
|
12
|
+
* dans le profil, pas dans un contexte isolé — puis Lighthouse se branche sur ce
|
|
13
|
+
* même navigateur et hérite du profil. Sans cela, il mesurerait l'écran de
|
|
14
|
+
* connexion en croyant tenir la page demandée.
|
|
15
|
+
*
|
|
16
|
+
* `@usage` node audit.mjs /tableau-de-bord
|
|
17
|
+
* `@env` NF_BROWSER_BASE origine à joindre (défaut constaté : local ou conteneur)
|
|
18
|
+
* `@env` NF_BROWSER_OUT dossier de sortie (le rapport complet y est déposé)
|
|
19
|
+
* `@env` NF_BROWSER_LOGIN chemin du formulaire de connexion — aucun défaut deviné
|
|
20
|
+
* `@env` NF_BROWSER_USER identifiant ; sans lui, aucune authentification n'est tentée
|
|
21
|
+
* `@env` NF_BROWSER_PASSWORD mot de passe associé
|
|
22
|
+
* `@env` NF_BROWSER_CATEGORIES catégories à jouer, séparées par des virgules (défaut : toutes celles que ce Lighthouse connaît)
|
|
23
|
+
* `@env` NF_BROWSER_FORMFACTOR `desktop` (défaut) ou `mobile` — un score de performance ne veut RIEN dire sans son appareil
|
|
24
|
+
* `@env` NF_BROWSER_SEUIL_AUDIT score en deçà duquel un audit est retenu, en pourcentage (défaut 90)
|
|
25
|
+
* `@requires` `lighthouse` et `playwright` installés (pairs optionnels)
|
|
26
|
+
* `@output` un résumé JSON sur stdout + le rapport COMPLET dans le dossier de sortie
|
|
27
|
+
* `@exit` 0 mesure rendue (le verdict est une DONNÉE) · 64 usage · 69 outil indisponible
|
|
28
|
+
*/
|
|
29
|
+
import { createServer } from "node:net";
|
|
30
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
import { mkdtempSync } from "node:fs";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { BASE, LOGIN, PASSWORD, OUTPUT, USER } from "./lib/browser.mjs";
|
|
35
|
+
import { summarizeLighthouse } from "./lib/probes.mjs";
|
|
36
|
+
|
|
37
|
+
const PAGE = process.argv[2] ?? process.env.NF_BROWSER_PAGE ?? "/";
|
|
38
|
+
const THRESHOLD = Number(process.env.NF_BROWSER_SEUIL_AUDIT ?? 90) / 100;
|
|
39
|
+
const FORMFACTOR = (process.env.NF_BROWSER_FORMFACTOR ?? "desktop").trim();
|
|
40
|
+
if (FORMFACTOR !== "desktop" && FORMFACTOR !== "mobile") {
|
|
41
|
+
console.error(
|
|
42
|
+
`NF_BROWSER_FORMFACTOR inconnu : « ${FORMFACTOR} ». Valeurs : desktop, mobile.`,
|
|
43
|
+
);
|
|
44
|
+
process.exit(64); // EX_USAGE
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tools = {};
|
|
48
|
+
for (const name of ["lighthouse", "playwright"]) {
|
|
49
|
+
try {
|
|
50
|
+
tools[name] = await import(name);
|
|
51
|
+
} catch {
|
|
52
|
+
console.error(
|
|
53
|
+
`${name} est absent — cet audit ne peut pas avoir lieu sans lui.\n\n` +
|
|
54
|
+
` npm i -D lighthouse playwright && npx playwright install chromium\n\n` +
|
|
55
|
+
"Les deux sont des pairs OPTIONNELS : seuls ceux qui auditent une page\n" +
|
|
56
|
+
"les installent, personne ne les paie sans les vouloir.",
|
|
57
|
+
);
|
|
58
|
+
process.exit(69); // EX_UNAVAILABLE
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const lighthouse = tools.lighthouse.default;
|
|
62
|
+
const { chromium } = tools.playwright;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Un port libre, demandé au système plutôt que choisi au hasard.
|
|
66
|
+
*
|
|
67
|
+
* Un port fixe entrerait en collision avec un autre navigateur de débogage —
|
|
68
|
+
* et l'audit se brancherait alors sur le mauvais, ce qui ne produit pas une
|
|
69
|
+
* erreur mais une mesure d'une AUTRE page.
|
|
70
|
+
*
|
|
71
|
+
* @returns {Promise<number>} un port que rien n'écoute au moment du rendu.
|
|
72
|
+
*/
|
|
73
|
+
function freePort() {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const srv = createServer();
|
|
76
|
+
srv.on("error", reject);
|
|
77
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
78
|
+
const { port } = srv.address();
|
|
79
|
+
srv.close(() => resolve(port));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const port = await freePort();
|
|
85
|
+
// Profil PERSISTANT : c'est lui qui porte la session entre notre connexion et
|
|
86
|
+
// l'onglet que Lighthouse ouvrira. Jetable — il vit le temps de l'audit.
|
|
87
|
+
const profile = mkdtempSync(path.join(tmpdir(), "nf-audit-"));
|
|
88
|
+
const ctx = await chromium.launchPersistentContext(profile, {
|
|
89
|
+
channel: "chromium",
|
|
90
|
+
ignoreHTTPSErrors: true,
|
|
91
|
+
args: [`--remote-debugging-port=${port}`, "--no-sandbox"],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const page = ctx.pages()[0] ?? (await ctx.newPage());
|
|
96
|
+
if (USER) {
|
|
97
|
+
if (!LOGIN) {
|
|
98
|
+
console.error(
|
|
99
|
+
"NF_BROWSER_USER est posé mais pas NF_BROWSER_LOGIN : donne le chemin de\n" +
|
|
100
|
+
"ton formulaire de connexion, sinon l'audit mesurerait l'écran de connexion.",
|
|
101
|
+
);
|
|
102
|
+
process.exit(64); // EX_USAGE
|
|
103
|
+
}
|
|
104
|
+
await page.goto(`${BASE}${LOGIN}`, { waitUntil: "domcontentloaded" });
|
|
105
|
+
const id = page.getByRole("textbox", {
|
|
106
|
+
name: /identifiant|utilisateur|username|e-?mail/iu,
|
|
107
|
+
});
|
|
108
|
+
const pw = page.getByRole("textbox", { name: /mot de passe|password/iu });
|
|
109
|
+
await id.or(pw).first().waitFor({ timeout: 15000 });
|
|
110
|
+
if ((await id.count()) > 0) {
|
|
111
|
+
await id.fill(USER);
|
|
112
|
+
await id.press("Enter");
|
|
113
|
+
}
|
|
114
|
+
await pw.fill(PASSWORD, { timeout: 15000 });
|
|
115
|
+
await pw.press("Enter");
|
|
116
|
+
await page.waitForURL((u) => !u.pathname.endsWith(LOGIN), {
|
|
117
|
+
timeout: 20000,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const target = `${BASE}${PAGE}`;
|
|
122
|
+
const categories = (process.env.NF_BROWSER_CATEGORIES ?? "")
|
|
123
|
+
.split(",")
|
|
124
|
+
.map((c) => c.trim())
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
|
|
127
|
+
const runnerResult = await lighthouse(target, {
|
|
128
|
+
port,
|
|
129
|
+
output: "json",
|
|
130
|
+
logLevel: "error",
|
|
131
|
+
formFactor: FORMFACTOR,
|
|
132
|
+
// Le bridage d'écran par défaut simule un téléphone : le laisser en place
|
|
133
|
+
// pendant qu'on demande `desktop` produirait un décor incohérent, et des
|
|
134
|
+
// chiffres qu'on ne saurait rattacher à aucun appareil réel.
|
|
135
|
+
screenEmulation:
|
|
136
|
+
FORMFACTOR === "desktop"
|
|
137
|
+
? { mobile: false, width: 1440, height: 900, deviceScaleFactor: 1 }
|
|
138
|
+
: undefined,
|
|
139
|
+
// 🔴 Sans ceci, Lighthouse VIDE le stockage avant de mesurer — donc les
|
|
140
|
+
// témoins de session — et audite l'écran de connexion en silence. C'est le
|
|
141
|
+
// piège central d'un audit derrière authentification.
|
|
142
|
+
disableStorageReset: true,
|
|
143
|
+
...(categories.length > 0 ? { onlyCategories: categories } : {}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const lhr = runnerResult?.lhr;
|
|
147
|
+
if (!lhr) {
|
|
148
|
+
console.error("Lighthouse n'a rendu aucun rapport.");
|
|
149
|
+
process.exit(69); // EX_UNAVAILABLE
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Le rapport COMPLET est conservé : le résumé sert à décider, l'original à
|
|
153
|
+
// vérifier — et à comparer dans le temps.
|
|
154
|
+
mkdirSync(OUTPUT, { recursive: true });
|
|
155
|
+
const stamp = new Date().toISOString().replace(/[:.]/gu, "-").slice(0, 19);
|
|
156
|
+
const slug = PAGE.replace(/\//gu, "-").replace(/^-/u, "") || "racine";
|
|
157
|
+
const fullPath = path.join(OUTPUT, `lighthouse-${slug}-${stamp}.json`);
|
|
158
|
+
writeFileSync(fullPath, JSON.stringify(lhr), "utf8");
|
|
159
|
+
|
|
160
|
+
console.log(
|
|
161
|
+
JSON.stringify(
|
|
162
|
+
{ ...summarizeLighthouse(lhr, THRESHOLD), fullReport: fullPath },
|
|
163
|
+
null,
|
|
164
|
+
2,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
} finally {
|
|
168
|
+
await ctx.close();
|
|
169
|
+
}
|