@starterlib/cli 0.10.14 → 0.11.0

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 CHANGED
@@ -31,45 +31,45 @@ Scripts d'une SPA :
31
31
 
32
32
  ## Commandes
33
33
 
34
- | Commande | Ce qu'elle fait |
35
- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
- | `starter-web start` | L'hote de developpement : sprite, simulations (remote), puis `ng serve` sous `STARTER_WEB_HOTE_DEV=1` (`--port N`) |
37
- | `starter-web verify` | Les etapes ci-dessous, dans l'ordre, arretees a la premiere qui echoue, dont le nom est ecrit |
38
- | `starter-web construire` | Sprite, `ng build`, prechargement de la police (hote), manifeste, precompression |
39
- | `starter-web sprite` | Pose le sprite hache (`public/`) et sa constante engendree (`sprite-genere.ts`) |
40
- | `starter-web manifeste` | Engendre `manifest.json` dans le dossier livre ; idempotent |
41
- | `starter-web precompresser` | Ecrit les variantes `.br` et `.gz`, et `precompression.index` (chaque variante et le condense de son source, que le serveur confronte) ; `starter-web precompresser <dossier>` pour un autre dossier |
42
- | `starter-web i18n:types` | Engendre `src/app/i18n/cles.ts`, le type des cles de chaque perimetre (`injecterTraduction<C>`) |
43
- | `starter-web simulations` | Recopie les stubs WireMock du service (`../src/mock`), corps compris, pour l'hote de developpement (`src/hote-dev/simulations.engendre.json`) |
44
- | `starter-web navigateurs` | Ecrit `.browserslistrc` : la liste browserslist du socle, tiree du plancher de la matrice (lot 276) |
45
- | `starter-web etapes` | Liste les etapes et les roles qu'elles concernent |
34
+ | Commande | Ce qu'elle fait |
35
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `starter-web start` | L'hote de developpement : sprite, simulations (remote), puis `ng serve` sous `STARTER_WEB_HOTE_DEV=1` (`--port N`) |
37
+ | `starter-web verify` | Les etapes ci-dessous, dans l'ordre, arretees a la premiere qui echoue, dont le nom est ecrit |
38
+ | `starter-web construire` | Sprite, `ng build`, prechargement de la police (hote), manifeste, precompression |
39
+ | `starter-web sprite` | Pose le sprite hache (`public/`) et sa constante engendree (`sprite-genere.ts`) |
40
+ | `starter-web manifeste` | Engendre `manifest.json` dans le dossier livre ; idempotent |
41
+ | `starter-web precompresser` | Ecrit les variantes `.br` et `.gz`, et `precompression.index` (chaque variante et le condense de son source, que le serveur confronte) ; `starter-web precompresser <dossier>` pour un autre dossier |
42
+ | `starter-web i18n:types` | Engendre `src/app/i18n/cles.ts`, le type des cles de chaque perimetre (`injecterTraduction<C>`) |
43
+ | `starter-web simulations` | Recopie les stubs WireMock du service (`../src/mock`), corps compris, pour l'hote de developpement (`src/hote-dev/simulations.engendre.json`) ; `--scenarios` liste et juge les scenarios nommes du remote (`src/hote-dev/scenarios.json`), chacun avec son adresse `?scenario=<nom>` (P08-10) |
44
+ | `starter-web navigateurs` | Ecrit `.browserslistrc` : la liste browserslist du socle, tiree du plancher de la matrice (lot 276) |
45
+ | `starter-web etapes` | Liste les etapes et les roles qu'elles concernent |
46
46
 
47
47
  Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peut pas s'executer
48
48
  (argument inconnu, projet illisible).
49
49
 
50
50
  ## Les etapes de `verify`
51
51
 
52
- | Etape | Role | Ce qu'elle garde |
53
- | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
- | `versions` | tous | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
55
- | `navigateurs` | tous | La liste browserslist de la SPA (`.browserslistrc`, ou `browserslist` de `package.json`) est celle du plancher de la matrice : ni absente, ni plus basse, ni plus large (lot 276) |
56
- | `dependances` | tous | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
57
- | `jetons` | tous | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
58
- | `format` | tous | `prettier --check .` |
59
- | `lint` | tous | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
60
- | `i18n` | tous | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, aide de chaque page (`<perimetre>.aide.<page>`), types a jour |
61
- | `test` | tous | `ng test --no-watch` |
62
- | `contrat` | tous | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
63
- | `simulations` | remote | Les stubs WireMock du service, tels que l'hote de developpement les rejoue, a jour (lot 269) ; sautee sans simulateur ; un stub hors du sous-ensemble est nomme |
64
- | `build` | tous | `starter-web construire` |
65
- | `vulnerabilites` | tous | `pnpm audit --prod` : HIGH et CRITICAL bloquent sauf exception DATEE de `securite/exceptions-npm.json` (un trimestre au plus) ; SBOM CycloneDX dans `dist/sbom-front.cdx.json`, a cote du dossier livre (jamais servie) ; licences AGPL, SSPL et inconnues refusees (lot 139) |
66
- | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
67
- | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
68
- | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
69
- | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
70
- | `autonome` | autonome | Aucune federation livree (`remoteEntry`, carte d'importation, `es-module-shims`) ; versions d'Angular et du socle egales a celles du jumeau (`starterWeb.jumeau`) ; poids de l'ecran (`starterWeb.ecran` : entree et seuil en Ko br), lu dans la carte de la construction (lot 240) |
71
- | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
72
- | `securite` | tous | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
52
+ | Etape | Role | Ce qu'elle garde |
53
+ | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `versions` | tous | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
55
+ | `navigateurs` | tous | La liste browserslist de la SPA (`.browserslistrc`, ou `browserslist` de `package.json`) est celle du plancher de la matrice : ni absente, ni plus basse, ni plus large (lot 276) |
56
+ | `dependances` | tous | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
57
+ | `jetons` | tous | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
58
+ | `format` | tous | `prettier --check .` |
59
+ | `lint` | tous | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
60
+ | `i18n` | tous | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, aide de chaque page (`<perimetre>.aide.<page>`), types a jour, formulations interdites par l'application (`starterWeb.formulationsInterdites`) |
61
+ | `test` | tous | `ng test --no-watch` |
62
+ | `contrat` | tous | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
63
+ | `simulations` | remote | Les stubs WireMock du service, tels que l'hote de developpement les rejoue, a jour (lot 269) ; sautee sans simulateur ni scenarios ; un stub hors du sous-ensemble est nomme ; le fichier de scenarios, s'il existe, juge (P08-10 : un scenario que l'hote ne rejouerait pas est un defaut) |
64
+ | `build` | tous | `starter-web construire` |
65
+ | `vulnerabilites` | tous | `pnpm audit --prod` : HIGH et CRITICAL bloquent sauf exception DATEE de `securite/exceptions-npm.json` (un trimestre au plus) ; SBOM CycloneDX dans `dist/sbom-front.cdx.json`, a cote du dossier livre (jamais servie) ; licences AGPL, SSPL et inconnues refusees (lot 139) |
66
+ | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
67
+ | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
68
+ | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
69
+ | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
70
+ | `autonome` | autonome | Aucune federation livree (`remoteEntry`, carte d'importation, `es-module-shims`) ; versions d'Angular et du socle egales a celles du jumeau (`starterWeb.jumeau`) ; poids de l'ecran (`starterWeb.ecran` : entree et seuil en Ko br), lu dans la carte de la construction (lot 240) |
71
+ | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
72
+ | `securite` | tous | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
73
73
 
74
74
  `--etapes a,b` n'en joue que certaines ; `--sauter a,b` en retire.
75
75
 
@@ -94,6 +94,12 @@ par un point d'entree vide dans la configuration `production`).
94
94
  neutralise UNE lecture de cookie du cadre (`DomAdapter.getCookie`, dans `main` faute de morceau
95
95
  partage), toute autre restant un defaut ;
96
96
  - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
97
+ - les formulations que l'application interdit dans ses textes : `starterWeb.formulationsInterdites`,
98
+ chemin d'un fichier JSON relatif a sa racine, qui liste des `formulations` (un `id`, des `textes`
99
+ ou un `motif`, des `langues` au besoin) et des `exceptions` (une `formulation`, une `cle`, une
100
+ `raison`, une `date`) ; l'etape `i18n` y cherche chaque texte sans casse ni accents et en mots
101
+ entiers, chaque motif tel quel (format detaille : `src/etapes/formulations.mjs`). Sans reglage,
102
+ rien n'est cherche : le socle ne connait aucune formulation ;
97
103
  - le prefixe des selecteurs : `prefix` du projet d'`angular.json` (ou `starterWeb.prefixe`) ;
98
104
  - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser` ;
99
105
  - le train requis et le socle construit : `remoteEntry.json`, ce que la federation a reellement
@@ -1,6 +1,6 @@
1
1
  # Matrice de compatibilite du socle frontal
2
2
 
3
- Verifiee le 2026-09-26 contre le registre npm (lot 154). Toute montee de version se fait
3
+ Verifiee le 2026-10-07 contre le registre npm (montee au dernier correctif de la ligne 22.2 ; avant elle, lot 154). Toute montee de version se fait
4
4
  **ligne par ligne, dans le meme commit**, et `pnpm versions:check` echoue si le
5
5
  `package.json` s'en ecarte (version non exacte, autre que celle de la matrice, paquet Angular absent
6
6
  de la matrice, greffon de federation d'une autre majeure.mineure qu'Angular, autre pnpm, Node sous le
@@ -9,29 +9,30 @@ lit CE document, publie avec lui, et ses essais le comparent a la matrice de `ve
9
9
 
10
10
  ⚠️ **Trois jours d'anciennete au moins** : `minimumReleaseAge: 4320` (`pnpm-workspace.yaml`) tient hors
11
11
  du verrou toute version publiee depuis moins de trois jours (constat A1405). Une version plus recente,
12
- meme corrective, attend : `@angular-architects/native-federation` 22.2.1 (publie le 2026-09-25) n'entre
13
- pas au 2026-09-27 ; la 22.2.0 (2026-09-24) y entre. Angular monte en 22.2.0 le 2026-09-27 (demande du
14
- proprietaire du 2026-09-26).
12
+ meme corrective, attend : au 2026-10-07, `axe-core` 4.14.0 (2026-10-05), `typescript-eslint` 8.71.1
13
+ (2026-10-05), `jsdom` 30.1.2 (2026-10-04), `@microsoft/api-extractor` 7.59.4 (2026-10-06) et `orval`
14
+ 8.40.0 (2026-10-04) n'entrent pas ; la version precedente de chacun y entre. Angular monte en 22.2.1 le
15
+ 2026-10-07 (correctif publie le 2026-09-30), apres la 22.2.0 du 2026-09-27.
15
16
 
16
17
  ## Train `2026.1`
17
18
 
18
19
  | Element | Version | Pourquoi cette version exactement |
19
20
  | -------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
- | `@angular/core`, `common`, `compiler`, `compiler-cli`, `forms`, `platform-browser`, `router` | `22.2.0` | Premiere de la ligne 22.2, publiee le 2026-09-23 (plus de trois jours au 2026-09-27) |
21
- | `@angular/cdk`, `aria` | `22.2.0` | Le paquet `aria` est stable depuis la 22 ; il porte les primitives sans style. `@angular/material` est retire : aucun composant du socle ne l'importe |
22
- | `@angular/elements` | `22.2.0` | Les custom elements de `@starterlib/elements` seulement (lot 299, decision du proprietaire du 2026-10-01) ; son pair exige le numero exact de `@angular/core`, d'ou 22.2.0 et non la 22.2.1 publiee |
23
- | `@angular/build`, `@angular/cli` | `22.2.0` | ⚠️ **Cadence differente de `core`.** L'outillage recoit des correctifs que le coeur ne recoit pas. Une matrice qui ne porterait qu'un seul numero mentirait des le premier correctif |
24
- | `@angular-devkit/schematics` | `22.2.0` | Le moteur des schematics du CLI d'Angular, au meme numero : `@starterlib/schematics` (lot 285) n'en importe rien a l'execution, ses essais le lancent (`SchematicTestRunner`) |
25
- | `ng-packagr` | `22.2.1` | Derniere stable de la ligne 22.2 (2026-09-23) |
26
- | `typescript` | `6.0.3` | ⚠️ **Piege.** La 7.0.2 est publiee, mais `@angular/compiler-cli@22.2.0` exige `>=6.0 <6.1`. Installer la derniere casse la compilation |
21
+ | `@angular/core`, `common`, `compiler`, `compiler-cli`, `forms`, `platform-browser`, `router` | `22.2.1` | Dernier correctif de la ligne 22.2, publie le 2026-09-30 (plus de trois jours au 2026-10-07) |
22
+ | `@angular/cdk`, `aria` | `22.2.1` | Le paquet `aria` est stable depuis la 22 ; il porte les primitives sans style. `@angular/material` est retire : aucun composant du socle ne l'importe |
23
+ | `@angular/elements` | `22.2.1` | Les custom elements de `@starterlib/elements` seulement (lot 299, decision du proprietaire du 2026-10-01) ; son pair exige le numero exact de `@angular/core`, d'ou le meme 22.2.1 |
24
+ | `@angular/build`, `@angular/cli` | `22.2.1` | ⚠️ **Cadence differente de `core`.** L'outillage recoit des correctifs que le coeur ne recoit pas. Une matrice qui ne porterait qu'un seul numero mentirait des le premier correctif |
25
+ | `@angular-devkit/schematics` | `22.2.1` | Le moteur des schematics du CLI d'Angular, au meme numero : `@starterlib/schematics` (lot 285) n'en importe rien a l'execution, ses essais le lancent (`SchematicTestRunner`) |
26
+ | `ng-packagr` | `22.2.4` | Derniere stable de la ligne 22.2 (2026-10-01) |
27
+ | `typescript` | `6.0.3` | ⚠️ **Piege.** La 7.0.2 est publiee, mais `@angular/compiler-cli@22.2.1` exige `>=6.0 <6.1`. Installer la derniere casse la compilation |
27
28
  | `node` | `>=24.15.0` | 24.x LTS, declare dans `engines` ; le meme plancher que la construction Maven des SPA (`frontend.node.version`) |
28
29
  | `pnpm` | `11.27.1` | Declare dans `packageManager` ; reglages dans `pnpm-workspace.yaml` (`allowBuilds`, `minimumReleaseAge`). Le meme pour la construction Maven des SPA (`frontend.pnpm.version` de `platform-parent`) |
29
- | `vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` | `5.0.1` | Lanceur officiel du constructeur `@angular/build:unit-test` ; un meme numero pour toute la famille (des numeros meles chargent deux copies du lanceur) |
30
+ | `vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` | `5.0.3` | Lanceur officiel du constructeur `@angular/build:unit-test` ; un meme numero pour toute la famille (des numeros meles chargent deux copies du lanceur) |
30
31
  | `playwright`, `@playwright/test` | `1.63.0` | Le navigateur des essais : un correctif change un rendu mesure au pixel |
31
- | `eslint` | `10.10.0` | `angular-eslint@22.5.0` accepte les majeurs 9 et 10 |
32
+ | `eslint` | `10.12.0` | `angular-eslint@22.5.0` accepte les majeurs 9 et 10 |
32
33
  | `angular-eslint` | `22.5.0` | Suit le majeur d'Angular |
33
- | `stylelint`, `postcss-scss` | `17.15.0`, `4.0.9` | Les feuilles du socle et des SPA (lot 178, `@starterlib/eslint-config/stylelint`) ; `postcss-scss` lit le SCSS |
34
- | `jsdom`, `axe-core` | `30.0.1`, `4.13.0` | Le DOM des essais unitaires et le controle d'accessibilite : un correctif change ce qu'un essai voit |
34
+ | `stylelint`, `postcss-scss` | `17.16.0`, `4.0.9` | Les feuilles du socle et des SPA (lot 178, `@starterlib/eslint-config/stylelint`) ; `postcss-scss` lit le SCSS |
35
+ | `jsdom`, `axe-core` | `30.1.1`, `4.13.0` | Le DOM des essais unitaires et le controle d'accessibilite : un correctif change ce qu'un essai voit |
35
36
 
36
37
  ## Prochain train : `2026.2` (prevu, non applique)
37
38
 
@@ -49,8 +50,8 @@ interfaces), avec `pnpm verify` et `banc-d-assemblage.sh --construire` verts :
49
50
 
50
51
  | Element | Version | Avertissement |
51
52
  | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
- | `@angular-architects/native-federation` | `22.2.0` | ⚠️ **A epingler a l'exact, jamais par etiquette.** Le 2026-09-12, l'etiquette `latest` du paquet pointait sur la **21.2.6**, publiee apres la 22.1.2. Un `pnpm add` sans version ramene donc un majeur de retard, en silence, et l'echec se manifeste au chargement d'un remote, loin de la cause. Version mesuree au spike du 2026-09-16. Sa majeure.mineure est celle d'Angular (22.2) : `versions:check` refuse un couple disjoint, et les deux montent dans le meme commit (235a) |
53
- | `orval` | `8.37.0` | Client d'API engendre depuis le contrat du service (ADR web 0008) : services `HttpClient` pour les ecritures, `httpResource` pour les lectures, aucun schema zod, donc aucune dependance a l'execution. Derniere stable au 2026-09-25 ; a epingler a l'exact, en dependance de developpement |
53
+ | `@angular-architects/native-federation` | `22.2.2` | ⚠️ **A epingler a l'exact, jamais par etiquette.** Le 2026-09-12, l'etiquette `latest` du paquet pointait sur la **21.2.6**, publiee apres la 22.1.2. Un `pnpm add` sans version ramene donc un majeur de retard, en silence, et l'echec se manifeste au chargement d'un remote, loin de la cause. Version mesuree au spike du 2026-09-16. Sa majeure.mineure est celle d'Angular (22.2) : `versions:check` refuse un couple disjoint, et les deux montent dans le meme commit (235a) |
54
+ | `orval` | `8.39.0` | Client d'API engendre depuis le contrat du service (ADR web 0008) : services `HttpClient` pour les ecritures, `httpResource` pour les lectures, aucun schema zod, donc aucune dependance a l'execution. Derniere stable de plus de trois jours au 2026-10-07 (8.39.0, publiee le 2026-09-30) ; a epingler a l'exact, en dependance de developpement |
54
55
  | `es-module-shims` | `2.8.4` | Dependance du greffon, et pas un detail : c'est ELLE qui cree la politique de types de confiance `es-module-shims`. Toute montee doit verifier que le nom de la politique n'a pas change, sinon la page devient blanche sans erreur serveur |
55
56
  | `@axe-core/playwright` | `4.13.0` | Les essais d'accessibilite de bout en bout de la coquille (Playwright) ; le meme numero qu'`axe-core`, qui fait le controle : deux moteurs d'une version differente ne signaleraient pas les memes violations |
56
57
 
@@ -69,6 +70,10 @@ maximum, fonction par fonction, de ce que les feuilles et le noyau emploient san
69
70
  Sous ce plancher, rien ne casse sans le dire : une surface sans `@starting-style` apparait d'un coup ; une
70
71
  couleur sans `light-dark()` n'existe pas, d'ou le plancher.
71
72
 
73
+ Au-dessus du plancher, AVEC repli (P08-03) : la requete de style d'une propriete personnalisee,
74
+ `@container style(--st-prefixe-de-ton: visible)` (Chrome 111, Safari 18, Firefox 151), affiche le prefixe
75
+ de ton (« Erreur : ») qu'une application demande visible ; en deca, il reste masque, et toujours lu.
76
+
72
77
  Ce plancher est plus haut que celui d'Angular 22.2 (« baseline widely available » au 2026-05-07 : Chrome et
73
78
  Edge 119, Firefox 119, Safari et iOS 17) : la construction ne vise pas plus bas que le socle n'en a besoin.
74
79
 
@@ -85,11 +90,11 @@ WebKit 26.6, Pixel 7 et iPhone 15 emules. Chromium et le telephone Android jouen
85
90
  poussee ; Firefox, WebKit et l'iPhone jouent les scenarios `@moteurs` et `@mobile` chaque semaine
86
91
  (`BANC_MOTEURS=tous`).
87
92
 
88
- | Moteur | Version mesuree | Plancher | Ce qui se degrade entre le plancher et la mesure |
89
- | ------------- | --------------- | -------- | ---------------------------------------------------------------------------------- |
90
- | Chromium/Edge | 153 | 123 | rien de connu : les types de confiance y sont appliques depuis bien avant 123 |
91
- | Firefox | 155 | 129 | la protection des puits DOM par les types de confiance, si la version ne les a pas |
92
- | Safari/iOS | 26.6 | 17.5 | idem ; la politique bloque toujours les scripts sans nonce |
93
+ | Moteur | Version mesuree | Plancher | Ce qui se degrade entre le plancher et la mesure |
94
+ | ------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
95
+ | Chromium/Edge | 153 | 123 | rien de connu : les types de confiance y sont appliques depuis bien avant 123 |
96
+ | Firefox | 155 | 129 | la protection des puits DOM par les types de confiance, si la version ne les a pas ; avant 151, le prefixe de ton reste masque sous `--st-prefixe-de-ton: visible` |
97
+ | Safari/iOS | 26.6 | 17.5 | idem ; la politique bloque toujours les scripts sans nonce ; avant 18, le prefixe de ton reste masque |
93
98
 
94
99
  **WebView.** Android System WebView suit Chromium, WKWebView suit le Safari du systeme : memes planchers
95
100
  (decisions mobiles, ADR 0043 du socle `api/starter`). **Revision** : a chaque montee de Playwright (nouveaux moteurs) et au plus
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.10.14",
3
+ "version": "0.11.0",
4
4
  "description": "Chaine de verification et de construction des SPA du socle frontal starter : starter-web verify, construire, sprite, manifeste, precompresser. Jamais en dependance de production.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Le fichier de scenarios d'un remote (P08-10) : `src/hote-dev/scenarios.json`, les etats nommes
3
+ * d'une page que l'hote de developpement ouvre par l'adresse (`?scenario=<nom>`,
4
+ * `@starterlib/testing/hote-dev`). `starter-web simulations --scenarios` les liste ; l'etape
5
+ * `simulations` de `verify` les juge.
6
+ *
7
+ * ```json
8
+ * {
9
+ * "scenarios": {
10
+ * "liste-vide": {
11
+ * "description": "Aucun element : l'etat vide de la liste",
12
+ * "stubs": [
13
+ * {
14
+ * "request": { "method": "GET", "urlPath": "/api/v1/elements" },
15
+ * "response": { "status": 200, "jsonBody": { "content": [], "nextCursor": null } }
16
+ * }
17
+ * ]
18
+ * },
19
+ * "gestionnaire": {
20
+ * "description": "Session d'un gestionnaire",
21
+ * "session": { "nomAffiche": "Gabriel Essai", "roles": ["GESTIONNAIRE"] }
22
+ * }
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * Un scenario porte une `description`, et au besoin des `stubs` (le sous-ensemble de WireMock que
28
+ * l'hote rejoue, corps EN LIGNE : `jsonBody` ou `body`) et une `session` (ce qu'elle change de la
29
+ * session simulee). Tout ecart est un DEFAUT : un scenario ecrit pour l'hote doit s'y rejouer. Les
30
+ * noms et les donnees sont fictifs : aucune donnee personnelle reelle.
31
+ */
32
+ import { existsSync, readFileSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { horsDuSousEnsemble } from './simulations.mjs';
35
+
36
+ /** Ou vit le fichier de scenarios, depuis la racine de la SPA. */
37
+ export const CHEMIN_DES_SCENARIOS = join('src', 'hote-dev', 'scenarios.json');
38
+
39
+ /** Le nom d'un scenario : celui qu'on tape dans l'adresse (`NOM_DE_SCENARIO` de l'hote). */
40
+ export const NOM_DE_SCENARIO = /^[a-z][a-z0-9-]{0,60}$/;
41
+
42
+ const CLES_DU_SCENARIO = new Set(['description', 'stubs', 'session']);
43
+
44
+ /** Ce que la session simulee d'un scenario peut changer (`ReponseDeSession` du noyau). */
45
+ const CLES_DE_SESSION = new Set([
46
+ 'authenticated',
47
+ 'userId',
48
+ 'csrfToken',
49
+ 'email',
50
+ 'nomAffiche',
51
+ 'langue',
52
+ 'theme',
53
+ 'roles',
54
+ 'permissionsAffichables',
55
+ 'mustChangePassword',
56
+ 'montantsMasques',
57
+ 'expiresAt',
58
+ 'absoluteExpiresAt',
59
+ ]);
60
+
61
+ const estUnObjet = (valeur) =>
62
+ typeof valeur === 'object' && valeur !== null && !Array.isArray(valeur);
63
+
64
+ /** Les defauts d'un scenario (vide : il se rejoue). */
65
+ function defautsDuScenario(nom, scenario) {
66
+ const defauts = [];
67
+ if (!NOM_DE_SCENARIO.test(nom)) {
68
+ defauts.push(`scenario "${nom}" : nom attendu en minuscules et tirets (^[a-z][a-z0-9-]*$).`);
69
+ }
70
+ if (!estUnObjet(scenario)) return [...defauts, `scenario "${nom}" : un objet est attendu.`];
71
+ for (const cle of Object.keys(scenario)) {
72
+ if (!CLES_DU_SCENARIO.has(cle)) defauts.push(`scenario "${nom}" : cle inconnue "${cle}".`);
73
+ }
74
+ if (typeof scenario.description !== 'string' || scenario.description.trim() === '') {
75
+ defauts.push(`scenario "${nom}" : description absente (ce que le scenario montre).`);
76
+ }
77
+ if (scenario.stubs !== undefined) {
78
+ if (!Array.isArray(scenario.stubs)) {
79
+ defauts.push(`scenario "${nom}" : stubs attendus en liste.`);
80
+ } else {
81
+ for (const [indice, stub] of scenario.stubs.entries()) {
82
+ const ou = `scenario "${nom}", stub ${String(indice)}`;
83
+ if (!estUnObjet(stub?.request) || !estUnObjet(stub?.response)) {
84
+ defauts.push(`${ou} : request et response attendus.`);
85
+ continue;
86
+ }
87
+ if (stub.response.bodyFileName !== undefined) {
88
+ defauts.push(`${ou} : corps en ligne attendu (jsonBody ou body), pas bodyFileName.`);
89
+ }
90
+ const raisons = horsDuSousEnsemble(stub);
91
+ if (raisons.length > 0) {
92
+ defauts.push(`${ou} : hors de ce que l'hote rejoue : ${raisons.join(', ')}.`);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ if (scenario.session !== undefined) {
98
+ if (!estUnObjet(scenario.session)) {
99
+ defauts.push(`scenario "${nom}" : session attendue en objet.`);
100
+ } else {
101
+ for (const cle of Object.keys(scenario.session)) {
102
+ if (!CLES_DE_SESSION.has(cle)) {
103
+ defauts.push(`scenario "${nom}" : session, cle inconnue "${cle}".`);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return defauts;
109
+ }
110
+
111
+ /**
112
+ * Lit et juge le fichier de scenarios : `{ chemin, scenarios, defauts }`, chaque scenario
113
+ * `{ nom, description, stubs, session }` (nombre de stubs, session changee ou non), ou `null` si la
114
+ * SPA n'en a pas.
115
+ */
116
+ export function lireLesScenarios(projet) {
117
+ const chemin = join(projet.racine, CHEMIN_DES_SCENARIOS);
118
+ if (!existsSync(chemin)) return null;
119
+ let contenu;
120
+ try {
121
+ contenu = JSON.parse(readFileSync(chemin, 'utf8'));
122
+ } catch (erreur) {
123
+ return {
124
+ chemin,
125
+ scenarios: [],
126
+ defauts: [
127
+ `${CHEMIN_DES_SCENARIOS} illisible : ${erreur instanceof Error ? erreur.message : String(erreur)}.`,
128
+ ],
129
+ };
130
+ }
131
+ if (!estUnObjet(contenu) || !estUnObjet(contenu.scenarios)) {
132
+ return {
133
+ chemin,
134
+ scenarios: [],
135
+ defauts: [`${CHEMIN_DES_SCENARIOS} : un objet "scenarios" est attendu.`],
136
+ };
137
+ }
138
+ const defauts = [];
139
+ for (const cle of Object.keys(contenu)) {
140
+ if (cle !== 'scenarios' && cle !== '$comment') {
141
+ defauts.push(`${CHEMIN_DES_SCENARIOS} : cle inconnue "${cle}".`);
142
+ }
143
+ }
144
+ const scenarios = [];
145
+ for (const [nom, scenario] of Object.entries(contenu.scenarios)) {
146
+ defauts.push(...defautsDuScenario(nom, scenario));
147
+ scenarios.push({
148
+ nom,
149
+ description: typeof scenario?.description === 'string' ? scenario.description : '',
150
+ stubs: Array.isArray(scenario?.stubs) ? scenario.stubs.length : 0,
151
+ session: estUnObjet(scenario?.session),
152
+ });
153
+ }
154
+ return { chemin, scenarios, defauts };
155
+ }
@@ -110,8 +110,11 @@ export function racineDesStubs(projet) {
110
110
  return null;
111
111
  }
112
112
 
113
- /** Les raisons pour lesquelles l'hote ne rejouerait pas ce stub (vide : il le rejoue). */
114
- function horsDuSousEnsemble(stub) {
113
+ /**
114
+ * Les raisons pour lesquelles l'hote ne rejouerait pas ce stub (vide : il le rejoue). Le fichier de
115
+ * scenarios (P08-10) les tient pour des defauts.
116
+ */
117
+ export function horsDuSousEnsemble(stub) {
115
118
  const raisons = [];
116
119
  const inconnues = (objet, admises, ou) => {
117
120
  for (const cle of Object.keys(objet ?? {})) {
@@ -25,19 +25,23 @@ import { natureDesFichiers } from '../livraison.mjs';
25
25
  export const BUDGETS_DU_REMOTE = { pageKo: 60, totalKo: 250, catalogueKo: 30 };
26
26
 
27
27
  /**
28
- * Les seuils de l'hote, en Ko brotli (ADR 0031) : 301 pour le premier ecran reel, 282 pour la
29
- * charge initiale, ramenes au socle 0.10.0 (releves et historique dans l'ADR). La charge initiale
30
- * est passee de 280 a 281 le 2026-09-29, decision du proprietaire : 280,4 Ko mesures avec le socle
31
- * 0.10.9, dont les controles d'empreinte S05 du lot 272 tournent au demarrage. Puis, le meme jour,
32
- * 282 et 301 (decision de l'orchestrateur sur autorisation du proprietaire) : la coquille mesuree
33
- * contre le socle 0.10.11 construit, avant publication, 281,0 et 300,1 Ko (+0,4 Ko chacun : titre
34
- * de page, banniere collee, marque textuelle, creneau de marque, demarrage determine, amorce).
28
+ * Les seuils de l'hote, en Ko brotli (ADR 0031) : 309 pour le premier ecran reel, 288 pour la
29
+ * charge initiale (ADR web 0005, D167). Leur histoire : ramenes au socle 0.10.0 (releves dans
30
+ * l'ADR 0031) ; la charge initiale passee de 280 a 281 le 2026-09-29, decision du proprietaire :
31
+ * 280,4 Ko mesures avec le socle 0.10.9, dont les controles d'empreinte S05 du lot 272 tournent au
32
+ * demarrage. Puis, le meme jour, 282 et 301 (decision de l'orchestrateur sur autorisation du
33
+ * proprietaire) : la coquille mesuree contre le socle 0.10.11 construit, avant publication, 281,0
34
+ * et 300,1 Ko (+0,4 Ko chacun : titre de page, banniere collee, marque textuelle, creneau de
35
+ * marque, demarrage determine, amorce). Puis 288 et 309 le 2026-10-07 : la coquille d'Aplomb
36
+ * mesuree contre la 0.11.0 construite, avant publication, 286,96 et 307,86 Ko (+5,1 et +6,2 Ko sur
37
+ * la 0.10.14 : barre inferieure, action critique et raccourci, ressource rafraichie, tronc), puis
38
+ * 287,4 et 308,3 Ko avec sa feuille de marque et la feuille de sa police de titre (+0,44 Ko).
35
39
  *
36
40
  * ⚠️ Ce ne sont pas la cible du plan SPA §2.8 (170 Ko). L'ecart restant est STRUCTUREL : un paquet
37
41
  * partage en singleton part ENTIER, parce que n'importe quel remote peut en avoir besoin. Un seuil
38
42
  * se baisse ; il ne se leve jamais sans mesure ecrite et sans date.
39
43
  */
40
- export const BUDGETS_DE_L_HOTE = { premierEcranKo: 301, chargeInitialeKo: 282, cibleDuPlanKo: 170 };
44
+ export const BUDGETS_DE_L_HOTE = { premierEcranKo: 309, chargeInitialeKo: 288, cibleDuPlanKo: 170 };
41
45
 
42
46
  /** Les imports DYNAMIQUES d'un module : les pages qu'il charge a la demande. */
43
47
  const IMPORT_DYNAMIQUE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Les formulations qu'une application interdit dans ses textes affiches : une recherche textuelle
3
+ * simple dans ses catalogues, a chaque `verify` ; ni un controle du design, ni un jugement du sens
4
+ * (lot P09-27 d'Aplomb, generalise). L'etape `i18n` la lance.
5
+ *
6
+ * L'application declare sa liste : `package.json#starterWeb.formulationsInterdites`, chemin d'un
7
+ * fichier JSON relatif a sa racine. Le socle n'en connait aucune ; sans reglage, rien n'est cherche.
8
+ *
9
+ * {
10
+ * "formulations": [
11
+ * { "id": "F01", "textes": ["garanti", "garantie"], "langues": ["fr"] },
12
+ * { "id": "F07", "motif": "(?<![\\p{L}\\p{N}])[A-Z]-\\d{2}(?![\\p{L}\\p{N}])" }
13
+ * ],
14
+ * "exceptions": [
15
+ * { "formulation": "F01", "cle": "app.legal.absence", "raison": "...", "date": "2026-10-07" }
16
+ * ]
17
+ * }
18
+ *
19
+ * - `textes` : chaque forme se nomme (aucune flexion devinee) ; la recherche ignore la casse et les
20
+ * accents, confond les blancs (espaces insecables compris) et les tirets, et ne trouve un texte
21
+ * qu'en mots entiers : « garanti » ne se trouve pas dans « garantit ».
22
+ * - `motif` : une expression reguliere (drapeau `u`), appliquee au texte tel quel, casse comprise.
23
+ * - `langues` : parmi `fr` et `en` ; les deux par defaut.
24
+ * - une exception se nomme, se justifie et se date, pour une cle et une formulation ; celle dont la
25
+ * cle appartient a un perimetre du projet sans y exister est perimee : refusee.
26
+ */
27
+
28
+ /** Les blancs et les tirets, confondus en une espace. */
29
+ const BLANCS_ET_TIRETS = /[\s\p{Pd}]+/gu;
30
+ const BORD_AVANT = '(?<![\\p{L}\\p{N}])';
31
+ const BORD_APRES = '(?![\\p{L}\\p{N}])';
32
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
33
+
34
+ /** Le texte tel que la recherche le compare : sans casse ni accents, blancs et tirets confondus. */
35
+ export function normaliser(texte) {
36
+ return texte.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(BLANCS_ET_TIRETS, ' ');
37
+ }
38
+
39
+ function echapper(texte) {
40
+ return texte.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ }
42
+
43
+ function texteNonVide(valeur) {
44
+ return typeof valeur === 'string' && valeur.trim() !== '';
45
+ }
46
+
47
+ /** Une formulation lue : `{ formulation }` prete a chercher, ou `{ defaut }`. */
48
+ function lireUneFormulation(entree, rang, langues, ids) {
49
+ const ou = `formulation ${String(rang + 1)}`;
50
+ if (entree === null || typeof entree !== 'object')
51
+ return { defaut: `${ou} : un objet est attendu.` };
52
+ const { id, textes, motif } = entree;
53
+ if (!texteNonVide(id)) return { defaut: `${ou} : "id" manquant.` };
54
+ if (ids.has(id)) return { defaut: `${id} : identifiant en double.` };
55
+ ids.add(id);
56
+ const cherchees = entree.langues ?? langues;
57
+ if (
58
+ !Array.isArray(cherchees) ||
59
+ cherchees.length === 0 ||
60
+ cherchees.some((langue) => !langues.includes(langue))
61
+ ) {
62
+ return { defaut: `${id} : "langues" parmi ${langues.join(', ')}.` };
63
+ }
64
+ const motifs = [];
65
+ if (textes !== undefined) {
66
+ if (
67
+ !Array.isArray(textes) ||
68
+ textes.length === 0 ||
69
+ textes.some((texte) => typeof texte !== 'string' || normaliser(texte).trim() === '')
70
+ ) {
71
+ return { defaut: `${id} : "textes", une liste de textes non vides, est attendu.` };
72
+ }
73
+ for (const texte of textes) {
74
+ const cherche = BORD_AVANT + echapper(normaliser(texte).trim()) + BORD_APRES;
75
+ motifs.push({ expression: new RegExp(cherche, 'u'), surLeTexteNormalise: true });
76
+ }
77
+ }
78
+ if (motif !== undefined) {
79
+ if (!texteNonVide(motif))
80
+ return { defaut: `${id} : "motif", une expression non vide, est attendu.` };
81
+ try {
82
+ motifs.push({ expression: new RegExp(motif, 'u'), surLeTexteNormalise: false });
83
+ } catch (erreur) {
84
+ return {
85
+ defaut: `${id} : motif illisible (${erreur instanceof Error ? erreur.message : String(erreur)}).`,
86
+ };
87
+ }
88
+ }
89
+ if (motifs.length === 0) return { defaut: `${id} : "textes" ou "motif" attendu.` };
90
+ return { formulation: { id, langues: cherchees, motifs } };
91
+ }
92
+
93
+ /**
94
+ * La liste lue et jugee : les formulations pretes a chercher, les exceptions, et les defauts du
95
+ * fichier. Une liste fautive se refuse entiere : elle ne cherche rien de travers.
96
+ */
97
+ export function lireLesFormulations(liste, langues) {
98
+ const defauts = [];
99
+ const formulations = [];
100
+ const exceptions = [];
101
+ if (liste === null || typeof liste !== 'object' || !Array.isArray(liste.formulations)) {
102
+ return {
103
+ formulations,
104
+ exceptions,
105
+ defauts: ['un objet { "formulations": [...] } est attendu.'],
106
+ };
107
+ }
108
+ const ids = new Set();
109
+ for (const [rang, entree] of liste.formulations.entries()) {
110
+ const lue = lireUneFormulation(entree, rang, langues, ids);
111
+ if (lue.defaut === undefined) formulations.push(lue.formulation);
112
+ else defauts.push(lue.defaut);
113
+ }
114
+ if (liste.exceptions !== undefined && !Array.isArray(liste.exceptions)) {
115
+ defauts.push('"exceptions" : une liste est attendue.');
116
+ }
117
+ for (const [rang, exception] of (Array.isArray(liste.exceptions)
118
+ ? liste.exceptions
119
+ : []
120
+ ).entries()) {
121
+ const ou = `exception ${String(rang + 1)}`;
122
+ if (exception === null || typeof exception !== 'object') {
123
+ defauts.push(`${ou} : un objet est attendu.`);
124
+ continue;
125
+ }
126
+ const { formulation, cle, raison, date } = exception;
127
+ if (!ids.has(formulation))
128
+ defauts.push(`${ou} : formulation "${String(formulation)}" inconnue.`);
129
+ else if (!texteNonVide(cle)) defauts.push(`${ou} : "cle" manquante.`);
130
+ else if (!texteNonVide(raison))
131
+ defauts.push(`${ou} (${cle}) : "raison" manquante : une exception se justifie.`);
132
+ else if (typeof date !== 'string' || !DATE.test(date)) {
133
+ defauts.push(`${ou} (${cle}) : "date" AAAA-MM-JJ manquante : une exception se date.`);
134
+ } else exceptions.push({ formulation, cle });
135
+ }
136
+ return { formulations, exceptions, defauts };
137
+ }
138
+
139
+ /**
140
+ * Les textes des catalogues ou une formulation interdite se trouve, hors exception ; puis les
141
+ * exceptions perimees. `catalogues` : `{ <perimetre>: { fr: {...}, en: {...} } }`.
142
+ */
143
+ export function jugerLesFormulations({ formulations, exceptions }, catalogues) {
144
+ const defauts = [];
145
+ const admises = new Set(exceptions.map(({ formulation, cle }) => `${formulation} ${cle}`));
146
+ for (const [perimetre, parLangue] of Object.entries(catalogues)) {
147
+ for (const [langue, catalogue] of Object.entries(parLangue)) {
148
+ if (catalogue === null || typeof catalogue !== 'object') continue;
149
+ for (const [cle, message] of Object.entries(catalogue)) {
150
+ if (typeof message !== 'string') continue;
151
+ const normalise = normaliser(message);
152
+ for (const { id, langues, motifs } of formulations) {
153
+ if (!langues.includes(langue) || admises.has(`${id} ${cle}`)) continue;
154
+ for (const { expression, surLeTexteNormalise } of motifs) {
155
+ const trouve = (surLeTexteNormalise ? normalise : message).match(expression);
156
+ if (trouve !== null) {
157
+ defauts.push(
158
+ `${perimetre}.${langue}.json : cle "${cle}", formulation interdite ${id} ("${trouve[0]}").`,
159
+ );
160
+ break;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ for (const { formulation, cle } of exceptions) {
168
+ const perimetre = cle.split('.')[0];
169
+ const parLangue = catalogues[perimetre];
170
+ if (parLangue === undefined) continue;
171
+ const presente = Object.values(parLangue).some(
172
+ (catalogue) =>
173
+ catalogue !== null && typeof catalogue === 'object' && Object.hasOwn(catalogue, cle),
174
+ );
175
+ if (!presente) {
176
+ defauts.push(
177
+ `exception ${formulation} : cle "${cle}" absente du perimetre "${perimetre}" (exception perimee, a retirer).`,
178
+ );
179
+ }
180
+ }
181
+ return defauts;
182
+ }
@@ -18,13 +18,16 @@
18
18
  * - l'hote porte les cles que `@starterlib/core` traduit par son catalogue
19
19
  * (`schemas/cles-exigees-du-socle.json`, constat A0837) ;
20
20
  * - le type des cles engendre (`src/app/i18n/cles.ts`, `starter-web i18n:types`) est a jour : une cle
21
- * inconnue dans `t()` est alors une erreur de compilation.
21
+ * inconnue dans `t()` est alors une erreur de compilation ;
22
+ * - aucun texte ne porte une formulation que l'application interdit
23
+ * (`starterWeb.formulationsInterdites`, voir `formulations.mjs`), hors exception nommee et datee.
22
24
  */
23
25
  import { existsSync, readFileSync } from 'node:fs';
24
- import { join } from 'node:path';
26
+ import { join, resolve } from 'node:path';
25
27
  import { fichiers, lireJson, relatif } from '../commun.mjs';
26
28
  import { dossierInstalle } from '../socle-installe.mjs';
27
29
  import { cheminDesTypesDeCles, typesDeCles } from '../construction/types-i18n.mjs';
30
+ import { jugerLesFormulations, lireLesFormulations } from './formulations.mjs';
28
31
 
29
32
  export const LANGUES = ['fr', 'en'];
30
33
 
@@ -264,6 +267,35 @@ function lire(chemin) {
264
267
  }
265
268
  }
266
269
 
270
+ /**
271
+ * La recherche des formulations interdites, si l'application en declare : `cherchees` vaut `null`
272
+ * sans reglage, le nombre de formulations cherchees sinon.
273
+ */
274
+ export function rechercherLesFormulations(projet, catalogues) {
275
+ const reglage = projet.paquet?.starterWeb?.formulationsInterdites;
276
+ if (reglage === undefined) return { defauts: [], cherchees: null };
277
+ if (typeof reglage !== 'string' || reglage.trim() === '') {
278
+ return {
279
+ defauts: [
280
+ "package.json : starterWeb.formulationsInterdites, le chemin d'un fichier JSON, est attendu.",
281
+ ],
282
+ cherchees: 0,
283
+ };
284
+ }
285
+ const liste = lire(resolve(projet.racine, reglage));
286
+ if (liste === null) {
287
+ return {
288
+ defauts: [`${reglage} : absent ou illisible (starterWeb.formulationsInterdites).`],
289
+ cherchees: 0,
290
+ };
291
+ }
292
+ const lue = lireLesFormulations(liste, LANGUES);
293
+ if (lue.defauts.length > 0) {
294
+ return { defauts: lue.defauts.map((defaut) => `${reglage} : ${defaut}`), cherchees: 0 };
295
+ }
296
+ return { defauts: jugerLesFormulations(lue, catalogues), cherchees: lue.formulations.length };
297
+ }
298
+
267
299
  /**
268
300
  * Les perimetres de catalogues d'un projet : ceux du manifeste (remote), ceux de
269
301
  * `starterWeb.i18n.perimetres` (application autonome, lot 240), `socle` pour l'hote.
@@ -331,6 +363,10 @@ export default {
331
363
  }
332
364
  }
333
365
 
366
+ // Les formulations que l'application interdit dans ses textes (P09-27 d'Aplomb, generalise).
367
+ const recherche = rechercherLesFormulations(projet, catalogues);
368
+ defauts.push(...recherche.defauts);
369
+
334
370
  // Le type des cles engendre, a jour.
335
371
  const cheminDesTypes = cheminDesTypesDeCles(projet.racine);
336
372
  const attendu = typesDeCles(perimetres, catalogues);
@@ -342,7 +378,11 @@ export default {
342
378
 
343
379
  return {
344
380
  defauts,
345
- resume: `${String(perimetres.length)} perimetre(s), ${String(cles)} cle(s) en fr et en en, messages lisibles, cles appelees presentes.`,
381
+ resume:
382
+ `${String(perimetres.length)} perimetre(s), ${String(cles)} cle(s) en fr et en en, messages lisibles, cles appelees presentes` +
383
+ (recherche.cherchees === null
384
+ ? '.'
385
+ : `, ${String(recherche.cherchees)} formulation(s) interdite(s) cherchee(s).`),
346
386
  };
347
387
  },
348
388
  };
@@ -11,15 +11,24 @@
11
11
  *
12
12
  * Sont lues les feuilles `.scss` et `.css` sous `src/`, et les styles EN LIGNE des composants
13
13
  * (`styles: \`...\``), que la copie de la coquille ne voyait pas.
14
+ *
15
+ * La MARQUE de la SPA (niveau 2 bis : `marque.json`, `marque.ts` ou `marque.mjs` a la racine) est
16
+ * rejouee ici a chaque verification, par le moteur des jetons installe : une valeur reprise qui fait
17
+ * tomber une paire sous son seuil rougit `verify`, paire nommee, avant la construction.
14
18
  */
15
19
  import { readFileSync } from 'node:fs';
16
20
  import { join } from 'node:path';
17
21
  import { fichiers, relatif } from '../commun.mjs';
22
+ import { fichierDeMarque, jugerLaMarque } from '../marque.mjs';
18
23
 
19
24
  /** Poses a l'execution par un composant du socle, donc absents des feuilles engendrees. */
20
25
  const POSES_A_L_EXECUTION = new Set([
21
26
  // st-app-shell mesure l'en-tete collant et publie sa hauteur (scroll-padding de la page).
22
27
  '--st-hauteur-entete',
28
+ // st-bottom-nav (P08-05) et l'action permanente flottante de st-app-shell (P08-06) publient la
29
+ // hauteur qu'elles couvrent au bas de la fenetre.
30
+ '--st-hauteur-pied',
31
+ '--st-hauteur-action',
23
32
  ]);
24
33
 
25
34
  /** Une lecture de jeton : son nom, et le caractere qui suit (`,` : avec repli ; `)` : sans). */
@@ -68,7 +77,7 @@ export function jugerLesJetons(feuilles, definis) {
68
77
  export default {
69
78
  nom: 'jetons',
70
79
  roles: ['remote', 'hote', 'autonome'],
71
- executer(projet) {
80
+ async executer(projet) {
72
81
  const socle = join(projet.racine, 'node_modules', '@starterlib', 'tokens', 'styles');
73
82
  const definis = new Set();
74
83
  for (const feuille of fichiers(socle).filter((chemin) => chemin.endsWith('.css'))) {
@@ -99,9 +108,19 @@ export default {
99
108
  };
100
109
  }
101
110
  const { defauts, lues } = jugerLesJetons(feuilles, definis);
111
+ let marque = '';
112
+ const chemin = fichierDeMarque(projet.racine);
113
+ if (chemin !== null) {
114
+ const nom = relatif(projet.racine, chemin);
115
+ const juge = await jugerLaMarque(projet.racine, chemin);
116
+ for (const defaut of juge.defauts) defauts.push(`${nom} : ${defaut}`);
117
+ if (juge.defauts.length === 0) marque = ` ; marque ${String(juge.valeurs.nom)} conforme`;
118
+ }
102
119
  return {
103
120
  defauts,
104
- resume: `${String(lues)} lecture(s) sans repli dans ${String(feuilles.length)} feuille(s), toutes definies.`,
121
+ resume:
122
+ `${String(lues)} lecture(s) sans repli dans ${String(feuilles.length)} feuille(s), toutes ` +
123
+ `definies${marque}.`,
105
124
  };
106
125
  },
107
126
  };
@@ -34,6 +34,49 @@ export function jugerLesSelecteurs(prefixe, sources) {
34
34
  return defauts;
35
35
  }
36
36
 
37
+ /**
38
+ * Les regles de style du socle (P08-11 : seul le socle fait le design) : stylelint pour les feuilles,
39
+ * le greffon pour les gabarits. Les lever exige une exemption NOMMEE et DATEE.
40
+ */
41
+ const REGLES_DE_STYLE =
42
+ /\b(?:color-no-hex|color-named|declaration-property-value-(?:dis)?allowed-list|property-disallowed-list|socle\/no-style-literal|socle\/variantes-reservees)\b/;
43
+
44
+ /** Un commentaire qui leve des regles : `stylelint-disable-next-line <regles> -- <raison>`. */
45
+ const EXEMPTION = /(stylelint|eslint)-disable(?:-next-line|-line)?\b(.*?)(?:\*\/|-->|$)/gm;
46
+
47
+ /** Une raison qui commence par sa date : `-- 2026-10-07 : <raison>`. */
48
+ const DATEE = /--\s*\d{4}-\d{2}-\d{2}\s*:?\s*\S/;
49
+
50
+ /**
51
+ * Les exemptions des regles de style (P08-11) dans les sources d'une SPA (`{ nom, texte }` : feuilles,
52
+ * gabarits, code) : chacune nommee ET datee (`-- AAAA-MM-JJ : <raison>`), pour se journaliser dans le
53
+ * CHANGELOG de l'application. Une levee de TOUTES les regles (sans liste) dans une feuille ou un
54
+ * gabarit compte comme une exemption de style. Rend `{ defauts, datees }`.
55
+ */
56
+ export function jugerLesExemptions(sources) {
57
+ const defauts = [];
58
+ let datees = 0;
59
+ for (const { nom, texte } of sources) {
60
+ const codeSeul = nom.endsWith('.ts');
61
+ for (const exemption of texte.matchAll(EXEMPTION)) {
62
+ const suite = exemption[2] ?? '';
63
+ const regles = suite.split('--')[0] ?? '';
64
+ const toutes = regles.trim() === '';
65
+ if (toutes ? codeSeul : !REGLES_DE_STYLE.test(regles)) continue;
66
+ if (DATEE.test(suite)) {
67
+ datees += 1;
68
+ continue;
69
+ }
70
+ const ligne = texte.slice(0, exemption.index).split('\n').length;
71
+ defauts.push(
72
+ `${nom}:${String(ligne)} : exemption de style sans date ni raison ; ecrire ` +
73
+ '`-- AAAA-MM-JJ : <raison>` et la journaliser dans le CHANGELOG de l application.',
74
+ );
75
+ }
76
+ }
77
+ return { defauts, datees };
78
+ }
79
+
37
80
  /** Le resultat d'une commande deleguee : `resume` si elle reussit, son code sinon. */
38
81
  function deleguer(projet, arguments_, resume) {
39
82
  const code = executer('pnpm', arguments_, projet.racine);
@@ -70,6 +113,21 @@ export const lint = {
70
113
  }));
71
114
  const selecteurs = jugerLesSelecteurs(projet.prefixe, sources);
72
115
  if (selecteurs.length > 0) return { defauts: selecteurs };
116
+ // P08-11 : une exemption de style est nommee et datee, jamais muette.
117
+ const exemptions = jugerLesExemptions(
118
+ fichiers(join(projet.racine, 'src'))
119
+ .filter((chemin) => /\.(scss|css|html|ts)$/.test(chemin))
120
+ .filter((chemin) => !chemin.includes(DOSSIER_DU_CLIENT_ENGENDRE))
121
+ .map((chemin) => ({
122
+ nom: relatif(projet.racine, chemin),
123
+ texte: readFileSync(chemin, 'utf8'),
124
+ })),
125
+ );
126
+ if (exemptions.defauts.length > 0) return { defauts: exemptions.defauts };
127
+ const ecartes =
128
+ exemptions.datees === 0
129
+ ? ''
130
+ : ` ; ${String(exemptions.datees)} exemption(s) de style datee(s), a journaliser dans le CHANGELOG`;
73
131
  // Les feuilles sous stylelint (lot 178) : jetons, pixels, plans, ordre du DOM. La configuration
74
132
  // etend celle du socle (@starterlib/eslint-config/stylelint) ; sans elle, rien n'est garde.
75
133
  if (!CONFIGURATIONS_STYLELINT.some((nom) => existsSync(join(projet.racine, nom)))) {
@@ -88,7 +146,7 @@ export const lint = {
88
146
  return deleguer(
89
147
  projet,
90
148
  ['exec', 'stylelint', 'src/**/*.{scss,css}', '--allow-empty-input'],
91
- `selecteurs en ${String(projet.prefixe)}-, eslint . et stylelint conformes.`,
149
+ `selecteurs en ${String(projet.prefixe)}-, eslint . et stylelint conformes${ecartes}.`,
92
150
  );
93
151
  },
94
152
  };
@@ -134,6 +134,24 @@ const EXCEPTIONS_DU_CADRE = [
134
134
  export const LECTURE_DE_COOKIE_DU_CADRE =
135
135
  /getCookie\((\w+)\)\{return [\w$]+\(document\.cookie,\1\)\}/g;
136
136
 
137
+ /**
138
+ * Les strategies de stockage de l'ORCHESTRATEUR de Native Federation
139
+ * (`@softarc/native-federation-orchestrator`, `options.mjs` : `localStorageEntry`,
140
+ * `sessionStorageEntry`), que le greffon 22.2.2 fait entrer dans le `main` d'un hote : deux fabriques
141
+ * etiquetees (`.type = "localStorage"`, `"sessionStorage"`) que l'orchestrateur n'appelle que si
142
+ * `initFederation` recoit un de ces stockages ; le defaut, `globalThis`, n'ecrit rien dans le
143
+ * navigateur. De cette forme exacte, chacune est neutralisee UNE fois dans tout le livrable ; tout
144
+ * autre acces au stockage, n'importe ou, reste un defaut (ADR 0014). Constat de la montee d'Aplomb
145
+ * a la 0.11.0, 2026-10-07.
146
+ */
147
+ export const STOCKAGES_DE_L_ORCHESTRATEUR =
148
+ /([\w$]+)=([\w$]+)=>\(([\w$]+),([\w$]+)\)=>\{let ([\w$]+)=\{get\(\)\{let ([\w$]+)=(localStorage|sessionStorage)\.getItem\(`\$\{\2\}\.\$\{String\(\3\)\}`\);return \6\?JSON\.parse\(\6\):[\w$]+\(\3,\4\)\},set\(([\w$]+)\)\{return \7\.setItem\(`\$\{\2\}\.\$\{String\(\3\)\}`,JSON\.stringify\(\8\)\),\5\},clear\(\)\{return \7\.setItem\(`\$\{\2\}\.\$\{String\(\3\)\}`,JSON\.stringify\(\4\)\),this\}\};return \5\};\1\.type="\7";Object\.freeze\(\1\);/g;
149
+
150
+ /** Les stockages qu'une fabrique de l'orchestrateur nomme, dans l'ordre ou le livrable les porte. */
151
+ export function stockagesDeLOrchestrateur(contenu) {
152
+ return [...contenu.matchAll(STOCKAGES_DE_L_ORCHESTRATEUR)].map((trouve) => trouve[7]);
153
+ }
154
+
137
155
  /** Les types d'un script EN LIGNE qui ne s'executent pas : des donnees (`esms-options`, cartes). */
138
156
  const SCRIPTS_DE_DONNEES = new Set([
139
157
  'esms-options',
@@ -175,16 +193,26 @@ function jugerUnePage(nom, contenu) {
175
193
  * Les defauts d'un fichier livre `nom` (relatif au dossier livre) de contenu `contenu`. `role` :
176
194
  * `remote`, `hote` ou `autonome` ; `partage` : le fichier est du code du cadre, partage par la
177
195
  * federation ; `cadreNeutralise` : la lecture de cookie du cadre d'une application autonome a deja
178
- * ete comptee, et se neutralise ici (une fois par fichier).
196
+ * ete comptee, et se neutralise ici (une fois par fichier) ; `orchestrateurNeutralise` : les
197
+ * strategies de stockage de l'orchestrateur de federation ont ete comptees, et se neutralisent ici.
179
198
  */
180
199
  export function jugerUnFichierLivre(
181
200
  nom,
182
201
  contenu,
183
- { role = 'remote', partage = false, declarees = [], cadreNeutralise = false } = {},
202
+ {
203
+ role = 'remote',
204
+ partage = false,
205
+ declarees = [],
206
+ cadreNeutralise = false,
207
+ orchestrateurNeutralise = false,
208
+ } = {},
184
209
  ) {
185
210
  if (cadreNeutralise) {
186
211
  contenu = contenu.replace(LECTURE_DE_COOKIE_DU_CADRE, 'getCookie($1){return null}');
187
212
  }
213
+ if (orchestrateurNeutralise) {
214
+ contenu = contenu.replace(STOCKAGES_DE_L_ORCHESTRATEUR, '$1=null;');
215
+ }
188
216
  // Une carte, ou sa variante compressee : `/x.js.map.br` se servirait aussi bien que l'original.
189
217
  if (/\.map(?:\.br|\.gz)?$/.test(nom)) {
190
218
  return [`${nom} : carte de source livree (sourceMap doit rester false en production).`];
@@ -257,6 +285,8 @@ export default {
257
285
  // Application autonome : la lecture de cookie du cadre, une seule fois dans tout le livrable.
258
286
  const autonome = projet.role === 'autonome';
259
287
  let lecturesDuCadre = 0;
288
+ // Les strategies de stockage de l'orchestrateur : au plus une de chaque sorte dans le livrable.
289
+ const strategies = [];
260
290
  for (const chemin of livres) {
261
291
  const nom = relatif(projet.sortie, chemin);
262
292
  const contenu = /\.(?:m?js|html|svg|css)$/.test(nom) ? readFileSync(chemin, 'utf8') : '';
@@ -265,12 +295,15 @@ export default {
265
295
  ? (contenu.match(LECTURE_DE_COOKIE_DU_CADRE)?.length ?? 0)
266
296
  : 0;
267
297
  lecturesDuCadre += duCadre;
298
+ const deLOrchestrateur = /\.m?js$/.test(nom) ? stockagesDeLOrchestrateur(contenu) : [];
299
+ strategies.push(...deLOrchestrateur);
268
300
  defauts.push(
269
301
  ...jugerUnFichierLivre(nom, contenu, {
270
302
  role: projet.role,
271
303
  partage: partages.has(nom),
272
304
  declarees,
273
305
  cadreNeutralise: duCadre === 1,
306
+ orchestrateurNeutralise: deLOrchestrateur.length > 0,
274
307
  }),
275
308
  );
276
309
  }
@@ -280,10 +313,23 @@ export default {
280
313
  'une seule est admise dans une application autonome.',
281
314
  );
282
315
  }
316
+ for (const stockage of new Set(strategies)) {
317
+ const nombre = strategies.filter((nomme) => nomme === stockage).length;
318
+ if (nombre > 1) {
319
+ defauts.push(
320
+ `strategie ${stockage} de l'orchestrateur de federation : ${String(nombre)} occurrences ; ` +
321
+ 'une seule est admise dans le livrable.',
322
+ );
323
+ }
324
+ }
283
325
  return {
284
326
  defauts,
285
327
  avertissements,
286
- resume: `${String(livres.length)} fichier(s) livre(s) dont ${String(partages.size)} du cadre partage, rien a signaler.`,
328
+ resume:
329
+ `${String(livres.length)} fichier(s) livre(s) dont ${String(partages.size)} du cadre partage, rien a signaler` +
330
+ (strategies.length === 0
331
+ ? '.'
332
+ : ` (strategies de stockage de l'orchestrateur de federation : ${strategies.join(', ')}, neutralisees).`),
287
333
  };
288
334
  },
289
335
  };
@@ -5,24 +5,38 @@
5
5
  *
6
6
  * Un stub que l'hote ne sait pas rejouer est nomme en avertissement (le BFF en profil `mock` le sert
7
7
  * toujours) ; un fichier de stub illisible ou un corps introuvable est un defaut.
8
+ *
9
+ * P08-10 : le fichier de scenarios du remote (`src/hote-dev/scenarios.json`), s'il existe, est juge
10
+ * aussi ; un scenario que l'hote ne rejouerait pas est un defaut.
8
11
  */
9
12
  import { existsSync, readFileSync } from 'node:fs';
10
13
  import { join } from 'node:path';
11
14
  import { relatif } from '../commun.mjs';
15
+ import { lireLesScenarios } from '../construction/scenarios.mjs';
12
16
  import { CHEMIN_DES_SIMULATIONS, engendrerLesSimulations } from '../construction/simulations.mjs';
13
17
 
14
18
  export default {
15
19
  nom: 'simulations',
16
20
  roles: ['remote'],
17
21
  async executer(projet) {
22
+ const scenarios = lireLesScenarios(projet);
23
+ const deScenarios = scenarios?.defauts ?? [];
18
24
  const resultat = await engendrerLesSimulations(projet);
19
25
  if (resultat === null) {
26
+ if (scenarios !== null) {
27
+ return {
28
+ defauts: deScenarios,
29
+ avertissements: [],
30
+ resume: `${String(scenarios.scenarios.length)} scenario(s) nomme(s) ; aucun simulateur WireMock.`,
31
+ };
32
+ }
20
33
  return {
21
34
  sautee:
22
35
  "aucun simulateur WireMock (../src/mock, ou starterWeb.simulations) : l'hote de developpement ne rejoue que ses simulations ecrites.",
23
36
  };
24
37
  }
25
- const { texte, stubs, avertissements, defauts, racine } = resultat;
38
+ const { texte, stubs, avertissements, racine } = resultat;
39
+ const defauts = [...resultat.defauts, ...deScenarios];
26
40
  if (defauts.length > 0) return { defauts, avertissements };
27
41
  const cible = join(projet.racine, CHEMIN_DES_SIMULATIONS);
28
42
  const chemin = relatif(projet.racine, cible);
@@ -35,7 +49,11 @@ export default {
35
49
  return {
36
50
  defauts: [],
37
51
  avertissements,
38
- resume: `${String(stubs.length)} stub(s) de ${relatif(projet.racine, racine)} rejoues par l'hote de developpement, a jour.`,
52
+ resume:
53
+ `${String(stubs.length)} stub(s) de ${relatif(projet.racine, racine)} rejoues par l'hote de developpement, a jour` +
54
+ (scenarios === null
55
+ ? '.'
56
+ : ` ; ${String(scenarios.scenarios.length)} scenario(s) nomme(s).`),
39
57
  };
40
58
  },
41
59
  };
package/src/lancer.mjs CHANGED
@@ -9,9 +9,11 @@ import { demarrer } from './construction/demarrer.mjs';
9
9
  import { ecrireLeManifeste } from './construction/manifeste.mjs';
10
10
  import { juger, precompresser } from './construction/precompresser.mjs';
11
11
  import { poserLeSprite } from './construction/sprite.mjs';
12
+ import { CHEMIN_DES_SCENARIOS, lireLesScenarios } from './construction/scenarios.mjs';
12
13
  import { ecrireLesSimulations } from './construction/simulations.mjs';
13
14
  import { ecrireLesTypesDeCles } from './construction/types-i18n.mjs';
14
15
  import { lireJson, relatif } from './commun.mjs';
16
+ import { ecrireLaFeuilleDeMarque, fichierDeMarque } from './marque.mjs';
15
17
  import { ecrireLesNavigateurs } from './navigateurs.mjs';
16
18
  import { versionDuSocle } from './matrice.mjs';
17
19
  import { ProjetIllisible, lireLeProjet } from './projet.mjs';
@@ -30,8 +32,12 @@ Commandes :
30
32
  precompresser ecrit les variantes .br et .gz (dossier livre, ou le dossier donne)
31
33
  i18n:types engendre le type des cles de chaque perimetre (src/app/i18n/cles.ts)
32
34
  simulations recopie les stubs WireMock du service pour l'hote de developpement
33
- (src/hote-dev/simulations.engendre.json)
35
+ (src/hote-dev/simulations.engendre.json) ; --scenarios liste et juge les
36
+ scenarios nommes du remote (src/hote-dev/scenarios.json, ?scenario=<nom>)
34
37
  navigateurs ecrit la liste browserslist du socle (.browserslistrc, plancher de la matrice)
38
+ jetons juge la marque de la SPA (marque.json, ou --marque <fichier>) par le moteur des
39
+ jetons installe, et ecrit sa feuille starter-tokens-<nom>.css dans la sortie de
40
+ construction (ou --sortie <dossier>) ; refuse une marque non conforme, paire nommee
35
41
  etapes liste les etapes de verify
36
42
 
37
43
  Options de verify :
@@ -43,6 +49,30 @@ Options du projet (valeurs attendues, lues sinon dans ses fichiers) :
43
49
  --nom <nom> --prefixe <prefixe> --pleine-page | --pas-pleine-page
44
50
  `;
45
51
 
52
+ /**
53
+ * `starter-web simulations --scenarios` (P08-10) : les scenarios nommes du remote, chacun avec
54
+ * l'adresse qui l'ouvre dans l'hote de developpement ; 1 sur un defaut du fichier.
55
+ */
56
+ function listerLesScenarios(projet, sortie) {
57
+ const lus = lireLesScenarios(projet);
58
+ if (lus === null) {
59
+ sortie.info(`scenarios : aucun fichier de scenarios (${CHEMIN_DES_SCENARIOS}).`);
60
+ return 0;
61
+ }
62
+ for (const defaut of lus.defauts) sortie.erreur(` - ${defaut}`);
63
+ sortie.info(
64
+ `scenarios : ${String(lus.scenarios.length)} dans ${relatif(projet.racine, lus.chemin)}`,
65
+ );
66
+ for (const scenario of lus.scenarios) {
67
+ const contenu = [
68
+ `${String(scenario.stubs)} stub(s)`,
69
+ ...(scenario.session ? ['session changee'] : []),
70
+ ].join(', ');
71
+ sortie.info(` ?scenario=${scenario.nom} : ${scenario.description} (${contenu})`);
72
+ }
73
+ return lus.defauts.length > 0 ? 1 : 0;
74
+ }
75
+
46
76
  /** Les arguments en `{ commande, positionnels, options }` ; leve sur une option inconnue. */
47
77
  export function lireLesArguments(argv) {
48
78
  const options = {};
@@ -85,6 +115,14 @@ export function lireLesArguments(argv) {
85
115
  options.port = valeur(indice, argument);
86
116
  indice += 1;
87
117
  break;
118
+ case '--marque':
119
+ options.marque = valeur(indice, argument);
120
+ indice += 1;
121
+ break;
122
+ case '--sortie':
123
+ options.sortie = valeur(indice, argument);
124
+ indice += 1;
125
+ break;
88
126
  case '--configuration':
89
127
  options.configuration = valeur(indice, argument);
90
128
  indice += 1;
@@ -92,6 +130,9 @@ export function lireLesArguments(argv) {
92
130
  case '--pleine-page':
93
131
  options.pleinePage = true;
94
132
  break;
133
+ case '--scenarios':
134
+ options.scenarios = true;
135
+ break;
95
136
  case '--pas-pleine-page':
96
137
  options.pleinePage = false;
97
138
  break;
@@ -112,6 +153,25 @@ export function lireLesArguments(argv) {
112
153
  return { commande, positionnels: reste, options };
113
154
  }
114
155
 
156
+ /** `starter-web jetons` : juge la marque, ecrit sa feuille ; 1 et chaque defaut si elle echoue. */
157
+ async function marquer(racine, marque, dossier, sortie) {
158
+ const chemin = marque ?? fichierDeMarque(racine);
159
+ if (chemin === null || chemin === undefined) {
160
+ sortie.erreur(
161
+ 'starter-web jetons : aucune marque (marque.json a la racine, ou --marque <fichier>).',
162
+ );
163
+ return 2;
164
+ }
165
+ const { valeurs, defauts, feuille } = await ecrireLaFeuilleDeMarque(racine, chemin, dossier);
166
+ if (defauts.length > 0) {
167
+ sortie.erreur(`jetons : marque ${String(valeurs?.nom)} non conforme :`);
168
+ for (const defaut of defauts) sortie.erreur(` - ${defaut}`);
169
+ return 1;
170
+ }
171
+ sortie.info(`jetons : marque ${valeurs.nom} conforme, ${relatif(racine, feuille)} ecrite.`);
172
+ return 0;
173
+ }
174
+
115
175
  function precompresserEtDire(dossier, sortie) {
116
176
  const { resume, defauts } = juger(precompresser(dossier));
117
177
  sortie.info(`precompression : ${resume}`);
@@ -157,6 +217,19 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
157
217
  return 2;
158
218
  }
159
219
 
220
+ // Une marque se juge et s'ecrit sans projet autour quand la sortie est donnee (un essai, un depot
221
+ // de marque) ; sinon la feuille part dans la sortie de construction du projet.
222
+ if (commande === 'jetons' && options.sortie !== undefined) {
223
+ try {
224
+ return await marquer(racine, options.marque, resolve(racine, options.sortie), sortie);
225
+ } catch (erreur) {
226
+ sortie.erreur(
227
+ `starter-web jetons : ${erreur instanceof Error ? erreur.message : String(erreur)}`,
228
+ );
229
+ return 1;
230
+ }
231
+ }
232
+
160
233
  // Un dossier donne se precompresse sans projet autour : une fixture reecrite, un dossier d'essai.
161
234
  if (commande === 'precompresser' && positionnels[0] !== undefined) {
162
235
  return precompresserEtDire(resolve(racine, positionnels[0]), sortie);
@@ -199,6 +272,8 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
199
272
  }
200
273
  case 'precompresser':
201
274
  return precompresserEtDire(projet.sortie, sortie);
275
+ case 'jetons':
276
+ return await marquer(projet.racine, options.marque, projet.sortie, sortie);
202
277
  case 'navigateurs': {
203
278
  const chemin = ecrireLesNavigateurs(projet.racine);
204
279
  sortie.info(`navigateurs : ${relatif(projet.racine, chemin)} (plancher du socle)`);
@@ -217,6 +292,7 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
217
292
  return 0;
218
293
  }
219
294
  case 'simulations': {
295
+ if (options.scenarios === true) return listerLesScenarios(projet, sortie);
220
296
  const resultat = await ecrireLesSimulations(projet);
221
297
  if (resultat === null) {
222
298
  sortie.info(
package/src/marque.mjs ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * La marque d'une SPA (niveau 2 bis de `@starterlib/tokens`) : `starter-web jetons --marque <fichier>`
3
+ * lit une table de valeurs, la fait juger par le moteur des jetons INSTALLE (les gardes de contraste
4
+ * n'existent qu'a un seul endroit, le socle, a la version meme qui tournera dans la page), et ecrit la
5
+ * feuille `starter-tokens-<nom>.css` dans la sortie de construction. `starter-web verify` rejoue le
6
+ * meme jugement (etape `jetons`) a chaque verification, tant qu'un fichier de marque est a la racine.
7
+ *
8
+ * Fichier de marque : `marque.json`, ou un module `marque.ts` / `marque.mjs` qui exporte la table
9
+ * (`export default`, ou `export const marque`). Forme : `ValeursDeMarque` du moteur (README des jetons).
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { join, resolve } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { dossierInstalle } from './socle-installe.mjs';
15
+
16
+ /** Les fichiers de marque reconnus a la racine de la SPA, dans l'ordre de recherche. */
17
+ export const FICHIERS_DE_MARQUE = ['marque.json', 'marque.ts', 'marque.mjs'];
18
+
19
+ /** Le fichier de marque de la SPA, `null` si elle n'en a pas. */
20
+ export function fichierDeMarque(racine) {
21
+ for (const nom of FICHIERS_DE_MARQUE) {
22
+ const chemin = join(racine, nom);
23
+ if (existsSync(chemin)) return chemin;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ /** La table de valeurs d'un fichier de marque (JSON, ou module : Node 24 retire les types a l'import). */
29
+ export async function lireLaMarque(chemin) {
30
+ if (chemin.endsWith('.json')) return JSON.parse(readFileSync(chemin, 'utf8'));
31
+ const module = await import(pathToFileURL(chemin).href);
32
+ const valeurs = module.default ?? module.marque;
33
+ if (valeurs === undefined) {
34
+ throw new Error(`${chemin} n'exporte ni \`default\` ni \`marque\`.`);
35
+ }
36
+ return valeurs;
37
+ }
38
+
39
+ /** Le moteur des jetons de `@starterlib/tokens` INSTALLE ; leve s'il manque ou ne connait pas les marques. */
40
+ export async function moteurInstalle(racine) {
41
+ const dossier = dossierInstalle(racine, '@starterlib/tokens');
42
+ if (dossier === null)
43
+ throw new Error("@starterlib/tokens n'est pas installe : pnpm install d'abord.");
44
+ const module = join(dossier, 'fesm2022', 'starterlib-tokens-moteur.mjs');
45
+ if (!existsSync(module)) {
46
+ throw new Error(`le moteur des jetons manque a @starterlib/tokens installe (${module}).`);
47
+ }
48
+ const moteur = await import(pathToFileURL(module).href);
49
+ if (
50
+ typeof moteur.defautsDeLaMarque !== 'function' ||
51
+ typeof moteur.feuilleDeMarque !== 'function'
52
+ ) {
53
+ throw new Error(
54
+ '@starterlib/tokens installe ne connait pas les marques (niveau 2 bis) : monter le socle.',
55
+ );
56
+ }
57
+ return moteur;
58
+ }
59
+
60
+ /** `{ valeurs, defauts }` : la marque du fichier, jugee par le moteur installe de la SPA. */
61
+ export async function jugerLaMarque(racine, chemin) {
62
+ const moteur = await moteurInstalle(racine);
63
+ const valeurs = await lireLaMarque(chemin);
64
+ return { moteur, valeurs, defauts: [...moteur.defautsDeLaMarque(valeurs)] };
65
+ }
66
+
67
+ /**
68
+ * Juge la marque de `chemin` et, conforme, ecrit sa feuille dans `sortie`. Rend
69
+ * `{ valeurs, defauts, feuille }` (`feuille` : le chemin ecrit, absent s'il y a des defauts).
70
+ */
71
+ export async function ecrireLaFeuilleDeMarque(racine, chemin, sortie) {
72
+ const { moteur, valeurs, defauts } = await jugerLaMarque(racine, resolve(racine, chemin));
73
+ if (defauts.length > 0) return { valeurs, defauts };
74
+ mkdirSync(sortie, { recursive: true });
75
+ const feuille = join(sortie, `starter-tokens-${valeurs.nom}.css`);
76
+ writeFileSync(feuille, moteur.feuilleDeMarque(valeurs));
77
+ return { valeurs, defauts, feuille };
78
+ }