@nodefony/devkit 10.0.0-alpha.9 → 10.0.0-beta.2
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/README.md +23 -12
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +4 -3
- package/dist/nodefony/controllers/DevkitController.js +2 -2
- package/dist/nodefony/controllers/McpController.js +3 -3
- package/dist/nodefony/service/DevkitService.js +4 -3
- package/docs/index.md +2 -2
- package/package.json +7 -7
- package/skills/nodefony-add-service/SKILL.md +22 -3
- package/skills/nodefony-browser/scripts/inspect.mjs +15 -13
- package/skills/nodefony-browser/scripts/lib/probes.mjs +8 -10
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +4 -4
- package/skills/nodefony-browser/scripts/socket.mjs +2 -2
- package/skills/nodefony-dev/SKILL.md +16 -6
- package/skills/nodefony-dev/scripts/docs.mjs +1 -1
- package/skills/nodefony-devops/references/image.md +1 -1
package/README.md
CHANGED
|
@@ -75,10 +75,13 @@ navigateur — d'où la commande, qui reste la porte utile.
|
|
|
75
75
|
npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
(ce qui manque), **`nodefony_symbols`** (ce qu'une
|
|
80
|
-
**`nodefony_card`** (par où commencer)
|
|
81
|
-
|
|
78
|
+
Sept outils : **`nodefony_inspect`** (ce qui est monté), **`nodefony_check`**
|
|
79
|
+
(ce qui manque — la commande s'appelle `doctor`), **`nodefony_symbols`** (ce qu'une
|
|
80
|
+
API du framework signifie), **`nodefony_card`** (par où commencer),
|
|
81
|
+
**`nodefony_docs`** (la documentation des modules installés), **`nodefony_admin_list`**
|
|
82
|
+
et **`nodefony_admin_call`** (lectures du data plane d'administration, jeton porteur
|
|
83
|
+
du scope `admin:read` exigé). Ce sont les mêmes briques que les commandes — une
|
|
84
|
+
source, plusieurs portes.
|
|
82
85
|
|
|
83
86
|
**Ce n'est pas un process de plus.** Depuis la révision `2026-07-28` du transport
|
|
84
87
|
« Streamable HTTP », un serveur MCP est un simple endpoint `POST` sans session :
|
|
@@ -162,7 +165,7 @@ connaît pas encore.
|
|
|
162
165
|
|
|
163
166
|
### Vos propres outils
|
|
164
167
|
|
|
165
|
-
Ces
|
|
168
|
+
Ces outils intégrés décrivent le framework ; ils ne savent rien de votre métier. Tout
|
|
166
169
|
module de votre application peut publier les siens en implémentant
|
|
167
170
|
`getMcpTools(): IMcpTool[]` :
|
|
168
171
|
|
|
@@ -203,12 +206,13 @@ c'est fermé par défaut, et c'est voulu. Détail et pièges :
|
|
|
203
206
|
|
|
204
207
|
## Les skills d'agent
|
|
205
208
|
|
|
206
|
-
Le paquet livre
|
|
209
|
+
Le paquet livre huit **skills** au format [Agent Skills](https://agentskills.io)
|
|
207
210
|
— la marche à suivre complète pour les tâches où un agent, sans eux, inventerait
|
|
208
211
|
du code : créer une ressource REST, ajouter un service injectable, réserver une
|
|
209
212
|
route à qui est habilité, ouvrir un canal temps réel, faire évoluer le schéma
|
|
210
|
-
d'une base sans la détruire,
|
|
211
|
-
|
|
213
|
+
d'une base sans la détruire, voir puis MESURER un écran dans un navigateur
|
|
214
|
+
piloté — plus un skill qui conduit toute tâche de développement et un qui décrit
|
|
215
|
+
l'exploitation en conteneur. Ils sont lus par tout client conforme (Claude Code, Cursor, Copilot,
|
|
212
216
|
VS Code, Codex, Goose…).
|
|
213
217
|
|
|
214
218
|
`nodefony create app` les met à disposition à la création. Après un
|
|
@@ -225,10 +229,12 @@ npx nodefony ai:sync # --dry-run pour voir sans écrire, --json pour
|
|
|
225
229
|
= nodefony-add-realtime-channel @nodefony/devkit
|
|
226
230
|
= nodefony-add-service @nodefony/devkit
|
|
227
231
|
= nodefony-browser @nodefony/devkit
|
|
232
|
+
= nodefony-dev @nodefony/devkit
|
|
233
|
+
= nodefony-devops @nodefony/devkit
|
|
228
234
|
= nodefony-migrate-schema @nodefony/devkit
|
|
229
235
|
= nodefony-protect-route @nodefony/devkit
|
|
230
236
|
|
|
231
|
-
0 posé(s) · 0 mis à jour ·
|
|
237
|
+
0 posé(s) · 0 mis à jour · 8 inchangé(s)
|
|
232
238
|
```
|
|
233
239
|
|
|
234
240
|
**Le préfixe `nodefony-` vous laisse la place.** Ces pointeurs arrivent dans
|
|
@@ -262,9 +268,14 @@ plus aucun paquet ne livre est **signalé, jamais supprimé** — vous avez pu e
|
|
|
262
268
|
|
|
263
269
|
## Configuration
|
|
264
270
|
|
|
265
|
-
| Clé
|
|
266
|
-
|
|
|
267
|
-
| `enabled`
|
|
271
|
+
| Clé | Type | Défaut | Rôle |
|
|
272
|
+
| -------------------- | ---------- | --------------- | ----------------------------------------------------- |
|
|
273
|
+
| `enabled` | `boolean` | `true` | Interrupteur du module |
|
|
274
|
+
| `mcp.enabled` | `boolean` | `true` | Interrupteur de la porte MCP |
|
|
275
|
+
| `mcp.allowedOrigins` | `string[]` | `[]` | Origines navigateur admises sur la porte MCP |
|
|
276
|
+
| `mcp.allowRemote` | `boolean` | `false` | Accepte un client hors de la boucle locale |
|
|
277
|
+
| `mcp.tools` | `string[]` | les sept outils | Outils exposés |
|
|
278
|
+
| `mcp.authorization` | objet | — | Autorisation OAuth de la porte (voir `docs/index.md`) |
|
|
268
279
|
|
|
269
280
|
La source unique est le schéma Zod de `nodefony/config/config.ts` : c'est lui qui
|
|
270
281
|
porte les défauts, les descriptions et la validation. Une clé inconnue ou mal
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
|
|
2
2
|
import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
3
3
|
import { buildCard } from "./nodefony/src/card.js";
|
|
4
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
5
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
5
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
6
6
|
import DevkitService_default from "./nodefony/service/DevkitService.js";
|
|
7
7
|
import DevkitController_default from "./nodefony/controllers/DevkitController.js";
|
|
8
8
|
import McpController_default from "./nodefony/controllers/McpController.js";
|
|
@@ -33,7 +33,8 @@ let DevkitModule = class DevkitModule extends Module {
|
|
|
33
33
|
* fautif nommé, plutôt qu'en `undefined.x` au premier appel en production.
|
|
34
34
|
*/
|
|
35
35
|
async onKernelRegister() {
|
|
36
|
-
|
|
36
|
+
const input = this.options;
|
|
37
|
+
this.options = defineDevkitConfig(input ?? {});
|
|
37
38
|
return this;
|
|
38
39
|
}
|
|
39
40
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
3
3
|
import { Controller, controller, route } from "@nodefony/framework";
|
|
4
4
|
//#region nodefony/controllers/DevkitController.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
3
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateParam.js";
|
|
4
4
|
import { ACCESS_TOKEN_VERIFIER, JsonRpcError, Nodefony, authorizeProtectedResource, checkMcpAccess, collectMcpTools, handleMcpMessage, jsonRpcFailure, mcpCallerRoles, protectedResourceMetadataUrl, readBearerHeader } from "nodefony";
|
|
5
5
|
import { Body, Controller, Headers, controller, route } from "@nodefony/framework";
|
|
6
6
|
//#region nodefony/controllers/McpController.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import defaults from "../config/config.js";
|
|
2
2
|
import { buildCard } from "../src/card.js";
|
|
3
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
4
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
5
5
|
import { Module, Nodefony, Service, extend, injectable, mcpDeclaredScopes } from "nodefony";
|
|
6
6
|
//#region nodefony/service/DevkitService.ts
|
|
7
7
|
/**
|
|
@@ -57,7 +57,8 @@ let DevkitService = class DevkitService extends Service {
|
|
|
57
57
|
module;
|
|
58
58
|
cfg;
|
|
59
59
|
constructor(module) {
|
|
60
|
-
const
|
|
60
|
+
const options = module.options;
|
|
61
|
+
const merged = extend(true, {}, defaults, options ?? {});
|
|
61
62
|
super("devkit", module.container, module.notificationsCenter, merged);
|
|
62
63
|
this.module = module;
|
|
63
64
|
this.cfg = merged;
|
package/docs/index.md
CHANGED
|
@@ -4,7 +4,7 @@ navTitle: devkit
|
|
|
4
4
|
lang: fr
|
|
5
5
|
module: "@nodefony/devkit"
|
|
6
6
|
topic: overview
|
|
7
|
-
audience: [
|
|
7
|
+
audience: [developer]
|
|
8
8
|
tags: [module, developpement, agent]
|
|
9
9
|
status: stable
|
|
10
10
|
updated: 2026-09-01
|
|
@@ -198,7 +198,7 @@ convient.
|
|
|
198
198
|
|
|
199
199
|
### Ajouter VOS outils — ce que l'agent ne peut pas deviner
|
|
200
200
|
|
|
201
|
-
Les
|
|
201
|
+
Les outils intégrés ci-dessus décrivent le framework. Ils ne savent rien de votre
|
|
202
202
|
métier — et c'est précisément ce qu'un agent invente le plus mal. N'importe quel
|
|
203
203
|
module de votre application peut donc en publier :
|
|
204
204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/devkit",
|
|
3
|
-
"version": "10.0.0-
|
|
3
|
+
"version": "10.0.0-beta.2",
|
|
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>",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"coverage": "vitest run --coverage"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@nodefony/framework": "^10.0.0-
|
|
29
|
-
"@nodefony/http": "^10.0.0-
|
|
30
|
-
"nodefony": "^10.0.0-
|
|
28
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
29
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
30
|
+
"nodefony": "^10.0.0-beta.2",
|
|
31
31
|
"zod": "^4.6.5",
|
|
32
32
|
"playwright": "^1.50.0",
|
|
33
33
|
"lighthouse": "^13.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@nodefony/framework": "^10.0.0-
|
|
37
|
-
"@nodefony/http": "^10.0.0-
|
|
38
|
-
"nodefony": "^10.0.0-
|
|
36
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
37
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
38
|
+
"nodefony": "^10.0.0-beta.2"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"axe-core": "4.13.0",
|
|
@@ -4,12 +4,14 @@ description: >
|
|
|
4
4
|
Crée un service injectable dans une application Nodefony par `nodefony create service`, et le
|
|
5
5
|
fait entrer dans le conteneur — la moitié qu'on oublie. Porte la distinction entre le nom de la
|
|
6
6
|
CLASSE et le nom de l'INSTANCE, les deux façons d'obtenir un service depuis un autre
|
|
7
|
-
(`@inject` au constructeur ou `container.get` à l'usage),
|
|
7
|
+
(`@inject` au constructeur ou `container.get` à l'usage), le choix de sa durée de vie
|
|
8
|
+
(un exemplaire pour l'application, ou un par requête), et le défaut mesuré qu'un service
|
|
8
9
|
écrit à la main produit : une classe qui compile, dont les tests passent, et que le conteneur
|
|
9
10
|
ignore. À charger AVANT d'écrire une classe de service ou d'appeler un service depuis un autre.
|
|
10
11
|
Déclencheurs : "crée un service", "un service métier", "logique métier partagée", "injecter une
|
|
11
12
|
dépendance", "container.get", "@injectable", "@services", "appeler un service depuis un autre",
|
|
12
|
-
"mon service est undefined", "le conteneur ne trouve pas mon service"
|
|
13
|
+
"mon service est undefined", "le conteneur ne trouve pas mon service", "un service par
|
|
14
|
+
requête", "un état propre à chaque requête", "scope request".
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
# add-service — un service que le conteneur connaît
|
|
@@ -58,7 +60,7 @@ et à l'introspection, et n'est construit qu'à la première requête qui le ré
|
|
|
58
60
|
|
|
59
61
|
## Deux noms, et ils ne servent pas à la même chose
|
|
60
62
|
|
|
61
|
-
C'est le piège n°1, et il ne produit aucune erreur — juste un `
|
|
63
|
+
C'est le piège n°1, et il ne produit aucune erreur — juste un `null` :
|
|
62
64
|
|
|
63
65
|
```ts
|
|
64
66
|
@injectable() // ← nomme la CLASSE : @inject("BillingService")
|
|
@@ -69,6 +71,23 @@ export class BillingService extends Service {
|
|
|
69
71
|
}
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
## Choisir sa durée de vie — un seul exemplaire, ou un par requête
|
|
75
|
+
|
|
76
|
+
Chaque requête a son propre conteneur (un scope, calque posé sur celui de l'application et jeté à
|
|
77
|
+
la fin ; en WebSocket, à la fin de la connexion). La portée décide où vit l'exemplaire :
|
|
78
|
+
|
|
79
|
+
| `@injectable(…)` | Exemplaires | Pour |
|
|
80
|
+
| ---------------------------------------- | --------------------------------------- | --------------------------------------------------------- |
|
|
81
|
+
| `()` ou `("nom")` — défaut `singleton` | un pour toute l'application | cache, pool, client : ce qui doit être unique |
|
|
82
|
+
| `({ scope: "transient" })` | un par résolution | un objet jetable sans état partagé |
|
|
83
|
+
| `({ name: "tenant", scope: "request" })` | un par requête, créé si elle le demande | ce qui naît et meurt avec la requête (`clean()` à la fin) |
|
|
84
|
+
|
|
85
|
+
Un service `request` reçoit le scope au constructeur (`super("tenant", scope, false)`, même nom
|
|
86
|
+
qu'au décorateur) ; `create service` n'en génère pas, il s'écrit à la main. Un singleton qui en
|
|
87
|
+
dépend est refusé au démarrage. Pour **lire** une donnée de la requête depuis un singleton, pas
|
|
88
|
+
besoin de portée : `RequestContext.getScope()` ou `RequestContext.get()` au moment de l'appel.
|
|
89
|
+
Détail : `node_modules/nodefony/docs/service.md`.
|
|
90
|
+
|
|
72
91
|
## Obtenir un service depuis un autre — deux voies, un choix
|
|
73
92
|
|
|
74
93
|
| Voie | Quand |
|
|
@@ -167,8 +167,8 @@ await page.addInitScript(() => {
|
|
|
167
167
|
if (window.__nfCsp.length < 20)
|
|
168
168
|
window.__nfCsp.push({
|
|
169
169
|
directive: e.violatedDirective,
|
|
170
|
-
blocked:
|
|
171
|
-
source:
|
|
170
|
+
blocked: (e.blockedURI ?? "").slice(0, 140),
|
|
171
|
+
source: (e.sourceFile ?? "").slice(0, 140),
|
|
172
172
|
line: e.lineNumber,
|
|
173
173
|
});
|
|
174
174
|
});
|
|
@@ -355,7 +355,7 @@ function effectiveBackground(el) {
|
|
|
355
355
|
let opaqueLayer = null;
|
|
356
356
|
for (let n = el; n; n = n.parentElement) {
|
|
357
357
|
const bg = getComputedStyle(n).backgroundColor;
|
|
358
|
-
if (!bg ||
|
|
358
|
+
if (!bg || bg.includes("transparent")) continue;
|
|
359
359
|
const { a } = parseColor(bg);
|
|
360
360
|
if (a === 0) continue;
|
|
361
361
|
if (a >= 1) {
|
|
@@ -397,7 +397,7 @@ function probeA11y() {
|
|
|
397
397
|
// simplifié suffit à attraper un bouton-icône muet — le cas réel.
|
|
398
398
|
const accessibleName = (el) => {
|
|
399
399
|
const aria = el.getAttribute("aria-label");
|
|
400
|
-
if (aria
|
|
400
|
+
if (aria?.trim()) return aria.trim();
|
|
401
401
|
const refs = el.getAttribute("aria-labelledby");
|
|
402
402
|
if (refs) {
|
|
403
403
|
const t = refs
|
|
@@ -682,14 +682,14 @@ async function measurePage(args) {
|
|
|
682
682
|
// CSP de l'application refuserait à bon droit) : c'est le pilote qui évalue.
|
|
683
683
|
const expression = `((args) => {
|
|
684
684
|
${sourceWcag()}
|
|
685
|
-
${describeElement}
|
|
686
|
-
${isVisible}
|
|
687
|
-
${effectiveBackground}
|
|
688
|
-
${probeA11y}
|
|
689
|
-
${probeRendering}
|
|
690
|
-
${probeWebStorage}
|
|
691
|
-
${readPerf}
|
|
692
|
-
return (${measurePage})(args);
|
|
685
|
+
${describeElement.toString()}
|
|
686
|
+
${isVisible.toString()}
|
|
687
|
+
${effectiveBackground.toString()}
|
|
688
|
+
${probeA11y.toString()}
|
|
689
|
+
${probeRendering.toString()}
|
|
690
|
+
${probeWebStorage.toString()}
|
|
691
|
+
${readPerf.toString()}
|
|
692
|
+
return (${measurePage.toString()})(args);
|
|
693
693
|
})(${JSON.stringify({ probes: PROBES, families: [...active] })})`;
|
|
694
694
|
|
|
695
695
|
const measured = await page.evaluate(expression);
|
|
@@ -711,7 +711,9 @@ if (active.has("axe")) {
|
|
|
711
711
|
try {
|
|
712
712
|
const axeCode = await axeSource();
|
|
713
713
|
const report = await page.evaluate(async (source) => {
|
|
714
|
-
//
|
|
714
|
+
// Le code d'axe-core est une CHAÎNE à exécuter dans la page : c'est le seul
|
|
715
|
+
// moyen de l'y charger sans `<script>` injecté, que la CSP refuserait.
|
|
716
|
+
// oxlint-disable-next-line no-new-func, typescript/no-implied-eval -- évalué par le pilote, hors CSP
|
|
715
717
|
new Function(source)();
|
|
716
718
|
return await window.axe.run(document, {
|
|
717
719
|
resultTypes: ["violations", "incomplete"],
|
|
@@ -39,7 +39,7 @@ export const FAMILIES = Object.freeze({
|
|
|
39
39
|
* et celles qui n'existent pas — à refuser, jamais à ignorer.
|
|
40
40
|
*/
|
|
41
41
|
export function parseFamilies(raw, fallback = []) {
|
|
42
|
-
const requested =
|
|
42
|
+
const requested = (raw ?? "").trim();
|
|
43
43
|
if (!requested) return { kept: [...fallback], unknown: [] };
|
|
44
44
|
if (requested === "toutes") {
|
|
45
45
|
return { kept: Object.keys(FAMILIES), unknown: [] };
|
|
@@ -71,7 +71,7 @@ export function parseFamilies(raw, fallback = []) {
|
|
|
71
71
|
export function parseProbes(raw) {
|
|
72
72
|
const probes = [];
|
|
73
73
|
const rejected = [];
|
|
74
|
-
for (const chunk of
|
|
74
|
+
for (const chunk of (raw ?? "")
|
|
75
75
|
.split(",")
|
|
76
76
|
.map((p) => p.trim())
|
|
77
77
|
.filter(Boolean)) {
|
|
@@ -171,9 +171,7 @@ export function parseActions(raw) {
|
|
|
171
171
|
* `schema` à null quand rien n'est demandé (on ne force rien).
|
|
172
172
|
*/
|
|
173
173
|
export function parseColorScheme(raw) {
|
|
174
|
-
const v =
|
|
175
|
-
.trim()
|
|
176
|
-
.toLowerCase();
|
|
174
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
177
175
|
if (!v) return { schema: null, invalid: null };
|
|
178
176
|
if (v === "light" || v === "dark" || v === "no-preference")
|
|
179
177
|
return { schema: v, invalid: null };
|
|
@@ -200,7 +198,7 @@ export function parseColorScheme(raw) {
|
|
|
200
198
|
export function parseStorage(raw) {
|
|
201
199
|
const entries = [];
|
|
202
200
|
const rejected = [];
|
|
203
|
-
for (const chunk of
|
|
201
|
+
for (const chunk of (raw ?? "")
|
|
204
202
|
.split(",")
|
|
205
203
|
.map((p) => p.trim())
|
|
206
204
|
.filter(Boolean)) {
|
|
@@ -261,7 +259,7 @@ export function environmentDefaults({ inContainer, base, out } = {}) {
|
|
|
261
259
|
* @returns {string} le nom de fichier, sans dossier.
|
|
262
260
|
*/
|
|
263
261
|
export function authStateName(login) {
|
|
264
|
-
const raw =
|
|
262
|
+
const raw = login ?? "";
|
|
265
263
|
const readable =
|
|
266
264
|
raw.replace(/[^A-Za-z0-9._-]/gu, "_").slice(0, 40) || "anonyme";
|
|
267
265
|
const fingerprint = createHash("sha256")
|
|
@@ -284,7 +282,7 @@ export function authStateName(login) {
|
|
|
284
282
|
export function parseWidths(raw) {
|
|
285
283
|
const widths = [];
|
|
286
284
|
const invalidWidths = [];
|
|
287
|
-
for (const chunk of
|
|
285
|
+
for (const chunk of (raw ?? "")
|
|
288
286
|
.split(",")
|
|
289
287
|
.map((p) => p.trim())
|
|
290
288
|
.filter(Boolean)) {
|
|
@@ -427,7 +425,7 @@ export function summarizeLighthouse(lhr, threshold = 0.9) {
|
|
|
427
425
|
for (const c of categories) {
|
|
428
426
|
for (const ref of c.auditRefs ?? []) {
|
|
429
427
|
const a = audits[ref.id];
|
|
430
|
-
if (
|
|
428
|
+
if (a?.score == null) continue;
|
|
431
429
|
if (a.score >= threshold) continue;
|
|
432
430
|
failed.push({
|
|
433
431
|
category: c.id,
|
|
@@ -495,7 +493,7 @@ export function summarizeLighthouse(lhr, threshold = 0.9) {
|
|
|
495
493
|
* @returns {string[]} les navigateurs à essayer, dans l'ordre.
|
|
496
494
|
*/
|
|
497
495
|
export function browserOrder(explicit) {
|
|
498
|
-
const v =
|
|
496
|
+
const v = (explicit ?? "").trim();
|
|
499
497
|
if (v) return [v];
|
|
500
498
|
return ["chromium", "chrome", "msedge"];
|
|
501
499
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @throws {Error} si la notation n'est pas reconnue.
|
|
40
40
|
*/
|
|
41
41
|
export function parseColor(color) {
|
|
42
|
-
const s =
|
|
42
|
+
const s = color.trim();
|
|
43
43
|
// `transparent` est le SEUL mot-clé que les navigateurs rendent parfois tel
|
|
44
44
|
// quel, et il a une valeur définie : `rgba(0, 0, 0, 0)`. Le refuser serait
|
|
45
45
|
// faux — un fond non peint est un cas courant, pas une erreur de mesure.
|
|
@@ -51,7 +51,7 @@ export function parseColor(color) {
|
|
|
51
51
|
if (hex) {
|
|
52
52
|
const d = hex[1];
|
|
53
53
|
if (d.length === 3 || d.length === 4) {
|
|
54
|
-
const q =
|
|
54
|
+
const q = Array.from(d, (c) => parseInt(c + c, 16));
|
|
55
55
|
return {
|
|
56
56
|
r: q[0],
|
|
57
57
|
g: q[1],
|
|
@@ -73,7 +73,7 @@ export function parseColor(color) {
|
|
|
73
73
|
const numbers = s.match(/-?\d*\.?\d+(?:e-?\d+)?%?/gi);
|
|
74
74
|
if (!numbers || numbers.length < 3)
|
|
75
75
|
throw new Error(
|
|
76
|
-
`notation de couleur non reconnue : ${JSON.stringify(
|
|
76
|
+
`notation de couleur non reconnue : ${JSON.stringify(color)} — ` +
|
|
77
77
|
"attendu rgb()/rgba(), color(srgb …) ou #rrggbb",
|
|
78
78
|
);
|
|
79
79
|
// `color(srgb …)` et `color(display-p3 …)` : canaux en 0–1. Le pourcentage
|
|
@@ -164,7 +164,7 @@ export function contrastRatio(a, b) {
|
|
|
164
164
|
* @returns {boolean} vrai si les seuils « texte large » s'appliquent.
|
|
165
165
|
*/
|
|
166
166
|
export function isLargeText(px, bold) {
|
|
167
|
-
return px >= 24 || (bold
|
|
167
|
+
return px >= 24 || (bold && px >= 18.66);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
@@ -320,7 +320,7 @@ async function scenarioSocket(conf) {
|
|
|
320
320
|
} catch (e) {
|
|
321
321
|
result.reconnection = {
|
|
322
322
|
closedAt,
|
|
323
|
-
verdict: `ÉCHEC — ${String(e
|
|
323
|
+
verdict: `ÉCHEC — ${String(e?.message ? e.message : e).slice(0, 140)}`,
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
326
|
result.closes = closes;
|
|
@@ -337,7 +337,7 @@ try {
|
|
|
337
337
|
// L'accueil qui ne vient jamais est LE symptôme à diagnostiquer en premier :
|
|
338
338
|
// endpoint faux, page non authentifiée, ou Origin refusé par le serveur.
|
|
339
339
|
console.error(
|
|
340
|
-
`Scénario interrompu : ${String(e
|
|
340
|
+
`Scénario interrompu : ${String(e?.message ? e.message : e).slice(0, 300)}\n` +
|
|
341
341
|
`→ vérifier le chemin du endpoint, l'authentification (NF_BROWSER_USER + NF_BROWSER_LOGIN), et que la page ${process.env.NF_BROWSER_PAGE ?? "/"} appartient bien à la même origine.`,
|
|
342
342
|
);
|
|
343
343
|
await browser.close();
|
|
@@ -240,6 +240,15 @@ npx nodefony stop <nom|chemin> # arrêter un AUTRE projet, sans change
|
|
|
240
240
|
npx nodefony production --detach --wait # boot réel en arrière-plan ; rend la main ports OUVERTS
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
+
**Déboguer** : en développement, les piles d'appels pointent vers les `.ts` de l'application (le
|
|
244
|
+
build de dev émet ses sourcemaps et le serveur les lit ; `npm run build`, lui, n'en émet pas — une
|
|
245
|
+
pile de production pointe vers `dist/`). Pour un point d'arrêt : `npm run debug` (= `nodefony debug` :
|
|
246
|
+
le mode développement, débogueur ouvert sur `127.0.0.1:9229`), puis `chrome://inspect` ou « Attach »
|
|
247
|
+
dans l'éditeur. `--inspect=<port>` change le port, `--inspect-brk` arrête avant le chargement de
|
|
248
|
+
l'application ; `nodefony development --inspect` fait la même chose. C'est le SERVEUR qui ouvre
|
|
249
|
+
l'inspecteur, et il le rouvre à chaque rechargement ; marche aussi avec `--no-watch`. ⚠️ Jamais `NODE_OPTIONS=--inspect` :
|
|
250
|
+
le premier process node de la chaîne (`npx`, `npm`) prend le port, et le serveur ne s'arrête jamais.
|
|
251
|
+
|
|
243
252
|
**Arrête ce que tu démarres.** Un serveur laissé derrière garde les ports : le run suivant échoue
|
|
244
253
|
sur une erreur qui ne parle jamais de lui (`EADDRINUSE`) — ou pire, un test interroge l'ANCIENNE
|
|
245
254
|
version du code. Et **jamais `… &`** : le processus reçoit SIGHUP et meurt ; tuer le PID du port ne
|
|
@@ -306,6 +315,8 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
|
|
|
306
315
|
- **Une clé de configuration mal orthographiée est retirée en silence.** `satisfies` sur le
|
|
307
316
|
fragment n'est pas décoratif : sans lui, la faute compile, puis la validation écarte la clé et
|
|
308
317
|
le module démarre sur son défaut. Personne ne le voit.
|
|
318
|
+
- **La config d'un module est FIGÉE après le démarrage** : l'écrire lève `TypeError`. Une variante
|
|
319
|
+
par requête = `overlayConfig()` (clés de la liste blanche du module), lue par `useConfig()`.
|
|
309
320
|
- **Tu lis une liste sans la BORNER.** Un `find` sans limite matérialise la table entière —
|
|
310
321
|
indolore sur les quelques lignes du poste de développement, fatal sur les dizaines de milliers
|
|
311
322
|
de la production. Le service d'une entité hérite `findPage({ limit: 25 })`.
|
|
@@ -370,12 +381,11 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
|
|
|
370
381
|
|
|
371
382
|
## 8. Données et fichiers — les façades qu'on ne recompose pas
|
|
372
383
|
|
|
373
|
-
- **Un
|
|
374
|
-
_stores_ qu'il sait tenir (`nodefony.stores` de son `package.json`) — `drizzle` les
|
|
375
|
-
`
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
chaque donnée.
|
|
384
|
+
- **Un cache ne remplace pas une base : ils se COMPLÈTENT.** Chaque adaptateur déclare les
|
|
385
|
+
_stores_ qu'il sait tenir (`nodefony.stores` de son `package.json`) — `drizzle` et `mongoose` les
|
|
386
|
+
huit, `redis` quatre (`session`, `tokens`, `passkeys`, `idempotency`) : un cache ne garde ni
|
|
387
|
+
comptes ni journal d'audit. Ne promets jamais une parité qui n'existe pas — lis ce champ, et
|
|
388
|
+
`npx nodefony inspect stores` dit où atterrit chaque donnée.
|
|
379
389
|
- **Les violations de contrainte sont DÉJÀ traduites en HTTP — ne les rattrape pas.** Un doublon
|
|
380
390
|
sur une colonne unique ressort en **409**, une donnée qui viole le schéma Zod en **422**, chacun
|
|
381
391
|
avec son corps JSON : le rendu d'erreur lit le code du pilote (`23505`, `ER_DUP_ENTRY`,
|
|
@@ -11,7 +11,7 @@ du frontal ne sort que si tu le demandes explicitement.
|
|
|
11
11
|
| ----------- | ------------------- | --------------------------------------------------------------------- | ----------------------------- |
|
|
12
12
|
| `build` | `node:24-alpine` | Installe, compile, élague les dépendances de développement | seulement son `/app` |
|
|
13
13
|
| `proxyconf` | hérite de `build` | **Dérive** la configuration nginx et rassemble les fichiers statiques | non — étage intermédiaire |
|
|
14
|
-
| `edge` | `nginx:1.
|
|
14
|
+
| `edge` | `nginx:1.30-alpine` | Le frontal, avec la configuration dérivée ci-dessus | non — cible séparée |
|
|
15
15
|
| _(final)_ | `node:24-alpine` | L'application en production | **oui** |
|
|
16
16
|
|
|
17
17
|
```bash
|