@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 (mesure des phases désactivée, ou requête interrompue)`);
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.4",
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.4",
31
- "@nodefony/framework": "^10.0.0-beta.4",
32
- "@nodefony/http": "^10.0.0-beta.4",
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.4",
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.1",
40
- "@nodefony/ai": "^10.0.0-beta.4",
41
- "@nodefony/framework": "^10.0.0-beta.4",
42
- "@nodefony/http": "^10.0.0-beta.4",
43
- "nodefony": "^10.0.0-beta.4"
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 naît au prochain démarrage en développement** (`CREATE TABLE IF NOT EXISTS`).
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). C'est LUI qui porte le verdict.
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
- for (let n = el; n; n = n.parentElement) {
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
- for (const el of document.querySelectorAll("body *")) {
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 > 0 && r.right > window.innerWidth + 1) {
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
- if (totalOutside >= 200) break;
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: overflowPx > 0 || failedFonts > 0 ? "ALERTE" : "OK",
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
- const el = document.querySelector(sel);
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();