@nodefony/devkit 10.0.0-beta.4 → 10.0.0-beta.5
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.
|
@@ -109,7 +109,7 @@ function gaps(report, ctx) {
|
|
|
109
109
|
if (report.requests === 0 && shot !== void 0) out.push("aucune réponse profilée" + (shot.client.failure === null ? "" : ` : ${shot.client.failure}`));
|
|
110
110
|
else if (report.requests === 0) out.push("aucune requête observée : l'application n'a reçu aucun trafic pendant la fenêtre (cet outil n'en génère pas — lancer la page, les tests ou une charge pendant l'observation)");
|
|
111
111
|
if (ctx.lost > 0) out.push(`${ctx.lost} profil(s) perdu(s) avant lecture (tampon du profileur débordé) : les chiffres portent sur un échantillon`);
|
|
112
|
-
if (report.untimed > 0) out.push(`${report.untimed} requête(s) sans durée (
|
|
112
|
+
if (report.untimed > 0) out.push(`${report.untimed} requête(s) sans durée (chronométrage coupé par timing.enabled: false)`);
|
|
113
113
|
if (ctx.interrupted) out.push(`${shot === void 0 ? "observation interrompue" : "tir interrompu"} après ${ms(ctx.elapsedMs / 1e3)} s : le serveur s'arrête (rechargement après une modification de fichier ?) — rappeler ` + (shot === void 0 ? "nodefony_observe" : "nodefony_profile_route") + " une fois le serveur redémarré, sans modifier de fichier pendant la mesure");
|
|
114
114
|
if (ctx.cancelled) out.push(shot === void 0 ? `observation abandonnée après ${ms(ctx.elapsedMs / 1e3)} s sur ${ms(ctx.windowMs / 1e3)} s` : `tir abandonné après ${ms(ctx.elapsedMs / 1e3)} s : ${shot.sent} requête(s) envoyée(s) sur ${shot.requested}`);
|
|
115
115
|
return out;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/devkit",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
|
|
6
6
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@nodefony/ai": "^10.0.0-beta.
|
|
31
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
32
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
30
|
+
"@nodefony/ai": "^10.0.0-beta.5",
|
|
31
|
+
"@nodefony/framework": "^10.0.0-beta.5",
|
|
32
|
+
"@nodefony/http": "^10.0.0-beta.5",
|
|
33
33
|
"lighthouse": "^13.0.0",
|
|
34
|
-
"nodefony": "^10.0.0-beta.
|
|
34
|
+
"nodefony": "^10.0.0-beta.5",
|
|
35
35
|
"playwright": "^1.50.0",
|
|
36
36
|
"zod": "^4.6.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "1.32.
|
|
40
|
-
"@nodefony/ai": "^10.0.0-beta.
|
|
41
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
42
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
43
|
-
"nodefony": "^10.0.0-beta.
|
|
39
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
40
|
+
"@nodefony/ai": "^10.0.0-beta.5",
|
|
41
|
+
"@nodefony/framework": "^10.0.0-beta.5",
|
|
42
|
+
"@nodefony/http": "^10.0.0-beta.5",
|
|
43
|
+
"nodefony": "^10.0.0-beta.5"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"axe-core": "4.14.0",
|
|
@@ -195,7 +195,11 @@ refus tombe tout seul.
|
|
|
195
195
|
|
|
196
196
|
## Les trois vérités à savoir avant de livrer (SQL)
|
|
197
197
|
|
|
198
|
-
1. **La table
|
|
198
|
+
1. **La table existe dès la commande quand l'application versionne ses migrations** (toute
|
|
199
|
+
application `complete`) : `create entity` écrit ET applique la migration de la table neuve —
|
|
200
|
+
`--no-migrate` s'en dispense ; si des entités modifiées attendaient, la migration est écrite mais
|
|
201
|
+
PAS appliquée, et la sortie nomme les tables en trop. Sinon, la table naît au prochain démarrage
|
|
202
|
+
en développement (`CREATE TABLE IF NOT EXISTS`).
|
|
199
203
|
2. **La modifier n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
|
|
200
204
|
créée n'apparaîtra pas dans une base existante.
|
|
201
205
|
3. **La production ne fabrique JAMAIS le schéma.** Elle l'applique par des migrations, écrites par
|
|
@@ -37,6 +37,11 @@ C'est au lecteur de juger — la sonde fournit, elle ne condamne pas ce qu'elle
|
|
|
37
37
|
Un sélecteur par élément (`NF_BROWSER_PROBES=libellé=sélecteur,…`) ; pour chacun : texte, couleur,
|
|
38
38
|
fond effectif, rapport de contraste, police, verdict WCAG, taille rendue.
|
|
39
39
|
|
|
40
|
+
- **Un widget en Shadow DOM est invisible à un sélecteur nu** — la sonde rend `absent`, et l'on
|
|
41
|
+
conclut à tort que l'élément n'existe pas. `hôte >>> sélecteur` entre dans la racine fantôme
|
|
42
|
+
OUVERTE de l'hôte (`#ma-barre >>> .bouton`), segment après segment. Le fond effectif traverse la
|
|
43
|
+
frontière de la même façon : au-dessus de la racine fantôme, c'est l'hôte qui fait fond.
|
|
44
|
+
|
|
40
45
|
- **Le fond effectif empile TOUTES les couches** jusqu'au premier ancêtre opaque, puis les compose.
|
|
41
46
|
Deux erreurs à ne pas refaire : lire `backgroundColor` sur l'élément rend `rgba(0,0,0,0)` et un
|
|
42
47
|
contraste faux ; s'arrêter à la première couche non transparente traite un voile à 13 % comme un
|
|
@@ -102,9 +107,16 @@ d'un `alt` restent un jugement humain.
|
|
|
102
107
|
## `rendu` — la page tient-elle dans son viewport, ses polices sont-elles là
|
|
103
108
|
|
|
104
109
|
- `horizontalOverflow` : la page dépasse-t-elle la largeur de la fenêtre (le défilement
|
|
105
|
-
horizontal accidentel).
|
|
110
|
+
horizontal accidentel). Il porte le verdict.
|
|
111
|
+
- `horizontalScrollers` : les conteneurs défilants (`overflow-x: auto|scroll`) dont le contenu
|
|
112
|
+
déborde — une barre de défilement horizontale DANS la page, que `horizontalOverflow` ne voit
|
|
113
|
+
pas. Il porte AUSSI le verdict. Vécu : une barre de débogage en Shadow DOM débordait de 232 px
|
|
114
|
+
pendant que la page entière se déclarait saine. Cause fréquente : une infobulle invisible
|
|
115
|
+
(`visibility: hidden`) ancrée à gauche sur un bouton de droite — invisible, elle élargit quand
|
|
116
|
+
même la zone défilable.
|
|
106
117
|
- `elementsOutsideViewport` : une **information**, pas un verdict — carrousels, tiroirs et textes
|
|
107
118
|
destinés aux lecteurs d'écran sortent du viewport légitimement.
|
|
119
|
+
- Les trois mesures descendent dans les **Shadow DOM ouverts**.
|
|
108
120
|
- `fonts` : ce que `document.fonts` a RÉELLEMENT chargé (statut par famille + graisse). Une
|
|
109
121
|
police en échec bascule le verdict — le texte s'affiche alors dans une police de repli, et toutes
|
|
110
122
|
les mesures de taille en héritent.
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* saisir, choisir, touche, voir, defiler, attendre. Un écran qui se déplie
|
|
28
28
|
* au clic n'existe pas tant qu'on ne l'a pas ouvert.
|
|
29
29
|
* `@env` NF_BROWSER_FULLPAGE 1 = capture la page ENTIÈRE (défaut : la fenêtre)
|
|
30
|
-
* `@env` NF_BROWSER_PROBES sélecteurs CSS à sonder, séparés par des virgules (`libellé=sélecteur`)
|
|
30
|
+
* `@env` NF_BROWSER_PROBES sélecteurs CSS à sonder, séparés par des virgules (`libellé=sélecteur`) ;
|
|
31
|
+
* `hôte >>> sélecteur` entre dans le Shadow DOM ouvert de l'hôte
|
|
31
32
|
* `@env` NF_BROWSER_WIDTHS largeurs de la famille responsive (défaut 360,768,1280)
|
|
32
33
|
* `@env` NF_BROWSER_VIEWPORT écran mesuré : mobile, mobile-court, tablette, bureau (défaut), large, tres-large, ou LARGEURxHAUTEUR — sous 600 px, émulé en téléphone (tactile, densité 2)
|
|
33
34
|
* `@env` NF_BROWSER_PRIMARY sélecteur de l'action principale pour la famille pli (défaut : le plus grand bouton de validation visible)
|
|
@@ -384,7 +385,14 @@ function isVisible(el) {
|
|
|
384
385
|
function effectiveBackground(el) {
|
|
385
386
|
const layers = [];
|
|
386
387
|
let opaqueLayer = null;
|
|
387
|
-
|
|
388
|
+
// Un élément posé à la racine d'un Shadow DOM n'a pas de parent : son fond
|
|
389
|
+
// est celui de l'HÔTE, et la remontée continue de l'autre côté.
|
|
390
|
+
const up = (n) => {
|
|
391
|
+
if (n.parentElement) return n.parentElement;
|
|
392
|
+
const root = n.getRootNode();
|
|
393
|
+
return root instanceof ShadowRoot ? root.host : null;
|
|
394
|
+
};
|
|
395
|
+
for (let n = el; n; n = up(n)) {
|
|
388
396
|
const bg = getComputedStyle(n).backgroundColor;
|
|
389
397
|
if (!bg || bg.includes("transparent")) continue;
|
|
390
398
|
const { a } = parseColor(bg);
|
|
@@ -550,18 +558,44 @@ function probeA11y() {
|
|
|
550
558
|
* Famille rendu — la page tient-elle dans son viewport, ses polices sont-elles
|
|
551
559
|
* VRAIMENT arrivées. Les éléments hors viewport sont une INFO, pas le verdict :
|
|
552
560
|
* carrousels et textes pour lecteurs d'écran en produisent légitimement.
|
|
561
|
+
*
|
|
562
|
+
* Le débordement se cherche AUSSI dans les Shadow DOM ouverts, et dans chaque
|
|
563
|
+
* conteneur défilant : un widget isolé (une barre de débogage) peut porter sa
|
|
564
|
+
* propre barre de défilement horizontale sans que la page, elle, déborde d'un
|
|
565
|
+
* pixel — la mesure du document seul la déclarait saine.
|
|
553
566
|
*/
|
|
554
567
|
function probeRendering() {
|
|
555
568
|
const doc = document.scrollingElement ?? document.documentElement;
|
|
556
569
|
const overflowPx = Math.max(0, doc.scrollWidth - window.innerWidth);
|
|
557
570
|
const outsideViewport = [];
|
|
558
571
|
let totalOutside = 0;
|
|
559
|
-
|
|
572
|
+
const scrollersX = [];
|
|
573
|
+
let totalScrollersX = 0;
|
|
574
|
+
const all = (root, out) => {
|
|
575
|
+
for (const el of root.querySelectorAll("*")) {
|
|
576
|
+
out.push(el);
|
|
577
|
+
if (el.shadowRoot) all(el.shadowRoot, out);
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
};
|
|
581
|
+
for (const el of all(document.body, [])) {
|
|
560
582
|
const r = el.getBoundingClientRect();
|
|
561
|
-
if (r.width
|
|
583
|
+
if (r.width === 0) continue;
|
|
584
|
+
if (r.right > window.innerWidth + 1 && totalOutside < 200) {
|
|
562
585
|
totalOutside += 1;
|
|
563
586
|
if (outsideViewport.length < 3) outsideViewport.push(describeElement(el));
|
|
564
|
-
|
|
587
|
+
}
|
|
588
|
+
const ox = getComputedStyle(el).overflowX;
|
|
589
|
+
if (
|
|
590
|
+
(ox === "auto" || ox === "scroll") &&
|
|
591
|
+
el.scrollWidth > el.clientWidth + 1
|
|
592
|
+
) {
|
|
593
|
+
totalScrollersX += 1;
|
|
594
|
+
if (scrollersX.length < 3)
|
|
595
|
+
scrollersX.push({
|
|
596
|
+
element: describeElement(el),
|
|
597
|
+
overflowPx: el.scrollWidth - el.clientWidth,
|
|
598
|
+
});
|
|
565
599
|
}
|
|
566
600
|
}
|
|
567
601
|
const families = {};
|
|
@@ -571,8 +605,12 @@ function probeRendering() {
|
|
|
571
605
|
if (f.status === "error") failedFonts += 1;
|
|
572
606
|
}
|
|
573
607
|
return {
|
|
574
|
-
verdict:
|
|
608
|
+
verdict:
|
|
609
|
+
overflowPx > 0 || totalScrollersX > 0 || failedFonts > 0
|
|
610
|
+
? "ALERTE"
|
|
611
|
+
: "OK",
|
|
575
612
|
horizontalOverflow: { present: overflowPx > 0, overflowPx },
|
|
613
|
+
horizontalScrollers: { total: totalScrollersX, examples: scrollersX },
|
|
576
614
|
elementsOutsideViewport: { total: totalOutside, examples: outsideViewport },
|
|
577
615
|
fonts: {
|
|
578
616
|
status: document.fonts.status,
|
|
@@ -659,7 +697,16 @@ async function measurePage(args) {
|
|
|
659
697
|
s.getAttribute("src"),
|
|
660
698
|
),
|
|
661
699
|
probes: args.probes.map(({ label, sel }) => {
|
|
662
|
-
|
|
700
|
+
// `hôte >>> sélecteur` traverse un Shadow DOM ouvert (un widget isolé,
|
|
701
|
+
// une barre de débogage) : chaque segment se cherche dans la racine
|
|
702
|
+
// fantôme de l'élément trouvé par le précédent.
|
|
703
|
+
let el = null;
|
|
704
|
+
let scope = document;
|
|
705
|
+
for (const part of sel.split(">>>")) {
|
|
706
|
+
el = scope?.querySelector(part.trim()) ?? null;
|
|
707
|
+
scope = el?.shadowRoot ?? null;
|
|
708
|
+
if (el === null) break;
|
|
709
|
+
}
|
|
663
710
|
if (!el) return { label, absent: true, selector: sel };
|
|
664
711
|
const cs = getComputedStyle(el);
|
|
665
712
|
const r = el.getBoundingClientRect();
|