@starterlib/cli 0.11.0 → 0.11.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 CHANGED
@@ -54,7 +54,7 @@ Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peu
54
54
  | `versions` | tous | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
55
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
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) |
57
+ | `jetons` | tous | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) ; la marque de la SPA (`marque.json`, `.ts` ou `.mjs`), rejouee par le moteur installe ; sa feuille versionnee (`starterWeb.feuilleDeMarque`), identique a celle qu'il engendre |
58
58
  | `format` | tous | `prettier --check .` |
59
59
  | `lint` | tous | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
60
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`) |
@@ -66,7 +66,7 @@ Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peu
66
66
  | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
67
67
  | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
68
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 |
69
+ | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : charge initiale et premier ecran (ses entrees, `starterWeb.premierEcran.entrees`, D171) |
70
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
71
  | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
72
72
  | `securite` | tous | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
@@ -89,10 +89,16 @@ par un point d'entree vide dans la configuration `production`).
89
89
  qu'un `manifest.source.json` existe ;
90
90
  - pour une application autonome, dans `starterWeb` : `nom` (le projet d'`angular.json`),
91
91
  `i18n.perimetres` (ses catalogues), `jumeau` (le dossier du remote du meme service, dont elle
92
- partage les versions) et `ecran` (`{ "entree": "src/app/pages/connexion.ts", "seuilKo": 155 }`) ;
92
+ partage les versions), `ecran` (`{ "entree": "src/app/pages/connexion.ts", "seuilKo": 155 }`) et, s'il en
93
+ cree, `trustedTypes` (les politiques Trusted Types qu'elle cree en plus de celles du BFF, que sa page doit porter
94
+ nommement : `["verification-humaine"]`) ;
93
95
  `starter-web construire` n'y ecrit pas de manifeste, et pose `--stats-json` ; `securite` y
94
96
  neutralise UNE lecture de cookie du cadre (`DomAdapter.getCookie`, dans `main` faute de morceau
95
97
  partage), toute autre restant un defaut ;
98
+ - pour l'hote, dans `starterWeb` : `premierEcran.entrees` (les sources de ses gabarits et de sa page d'arrivee,
99
+ `["src/app/gabarits/coquille.ts", "src/app/pages/accueil.ts"]`), cherchees dans `browser-stats.json` que la
100
+ cible esbuild d'`angular.json` ecrit avec `statsJson: true` ; sans elles, le premier ecran compte toutes les pages
101
+ de route (D171) ;
96
102
  - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
97
103
  - les formulations que l'application interdit dans ses textes : `starterWeb.formulationsInterdites`,
98
104
  chemin d'un fichier JSON relatif a sa racine, qui liste des `formulations` (un `id`, des `textes`
@@ -100,6 +106,10 @@ par un point d'entree vide dans la configuration `production`).
100
106
  `raison`, une `date`) ; l'etape `i18n` y cherche chaque texte sans casse ni accents et en mots
101
107
  entiers, chaque motif tel quel (format detaille : `src/etapes/formulations.mjs`). Sans reglage,
102
108
  rien n'est cherche : le socle ne connait aucune formulation ;
109
+ - la feuille de marque que la SPA versionne : `starterWeb.feuilleDeMarque`, chemin relatif a sa racine
110
+ (`marque/starter-tokens-<nom>.css`, ecrite par `starter-web jetons --sortie marque`) ; l'etape
111
+ `jetons` la veut identique a celle que le moteur installe engendre de sa marque, fins de ligne mises
112
+ a part. Sans reglage, seule la marque est jugee ;
103
113
  - le prefixe des selecteurs : `prefix` du projet d'`angular.json` (ou `starterWeb.prefixe`) ;
104
114
  - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser` ;
105
115
  - le train requis et le socle construit : `remoteEntry.json`, ce que la federation a reellement
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",
@@ -15,18 +15,19 @@
15
15
  * partages resolus par la carte d'importation.
16
16
  */
17
17
  import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
18
- import { join } from 'node:path';
18
+ import { dirname, join } from 'node:path';
19
19
  import { brotliCompressSync, constants } from 'node:zlib';
20
20
  import { cloture, departDeLaPage, totaux } from '../cloture.mjs';
21
21
  import { fichiers, ko, lireJson, relatif } from '../commun.mjs';
22
22
  import { natureDesFichiers } from '../livraison.mjs';
23
+ import { morceauDeLEntree } from './autonome.mjs';
23
24
 
24
25
  /** Les budgets d'un remote, en Ko brotli (plan SPA §2.8). */
25
26
  export const BUDGETS_DU_REMOTE = { pageKo: 60, totalKo: 250, catalogueKo: 30 };
26
27
 
27
28
  /**
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
29
+ * Les seuils de l'hote, en Ko brotli (ADR 0031) : 312 pour le premier ecran reel, mesure sur les
30
+ * entrees que l'hote declare, 289 pour la charge initiale (ADR web 0005, D168 et D171). Leur histoire : ramenes au socle 0.10.0 (releves dans
30
31
  * l'ADR 0031) ; la charge initiale passee de 280 a 281 le 2026-09-29, decision du proprietaire :
31
32
  * 280,4 Ko mesures avec le socle 0.10.9, dont les controles d'empreinte S05 du lot 272 tournent au
32
33
  * demarrage. Puis, le meme jour, 282 et 301 (decision de l'orchestrateur sur autorisation du
@@ -35,13 +36,23 @@ export const BUDGETS_DU_REMOTE = { pageKo: 60, totalKo: 250, catalogueKo: 30 };
35
36
  * marque, demarrage determine, amorce). Puis 288 et 309 le 2026-10-07 : la coquille d'Aplomb
36
37
  * mesuree contre la 0.11.0 construite, avant publication, 286,96 et 307,86 Ko (+5,1 et +6,2 Ko sur
37
38
  * 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).
39
+ * 287,4 et 308,3 Ko avec sa feuille de marque et la feuille de sa police de titre (+0,44 Ko). Puis
40
+ * 289 et 313 le 2026-10-07 : la coquille d'Aplomb refaite sur ses maquettes (menu en haut, barre
41
+ * inferieure, bandeau du compte simule, pied, accueil de simulation), mesuree contre la 0.11.1
42
+ * construite, avant publication, 288,2 et 312,2 Ko (tronc a 47 410 octets, D168). Puis 314 pour le
43
+ * premier ecran le 2026-10-07 (D169) : la meme coquille avec la garde du parcours d'ouverture (lecture
44
+ * du parcours avant l'accueil, client engendre compris), le gabarit pleine page refait et la page de
45
+ * repli du parcours, mesuree contre la 0.11.1 publiee et le tronc de la 0.11.2, 288,8 et 313,7 Ko.
46
+ * Puis 312 le 2026-10-08 (D171, avant la publication de la 0.11.2) : le premier ecran se mesure sur les
47
+ * ENTREES que l'hote declare (ses gabarits, sa page d'arrivee) et non plus sur toutes ses pages de route ;
48
+ * la coquille d'Aplomb avec le cablage du parcours, le contexte de compte, l'etat du bot (en `@defer`) et
49
+ * le bouton d'arret permanent, 311,3 Ko.
39
50
  *
40
51
  * ⚠️ Ce ne sont pas la cible du plan SPA §2.8 (170 Ko). L'ecart restant est STRUCTUREL : un paquet
41
52
  * partage en singleton part ENTIER, parce que n'importe quel remote peut en avoir besoin. Un seuil
42
53
  * se baisse ; il ne se leve jamais sans mesure ecrite et sans date.
43
54
  */
44
- export const BUDGETS_DE_L_HOTE = { premierEcranKo: 309, chargeInitialeKo: 288, cibleDuPlanKo: 170 };
55
+ export const BUDGETS_DE_L_HOTE = { premierEcranKo: 312, chargeInitialeKo: 289, cibleDuPlanKo: 170 };
45
56
 
46
57
  /** Les imports DYNAMIQUES d'un module : les pages qu'il charge a la demande. */
47
58
  const IMPORT_DYNAMIQUE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
@@ -158,25 +169,76 @@ function budgetDuRemote(projet) {
158
169
  };
159
170
  }
160
171
 
172
+ /**
173
+ * Le premier ecran de l'hote (D171) : la charge initiale (cloture de profondeur 1 depuis la page) et les
174
+ * ENTREES que l'hote declare (ses gabarits, sa page d'arrivee), chacune avec ses imports STATIQUES ; ni
175
+ * leurs imports dynamiques (un `@defer`, une page chargee a la demande), ni les autres pages de route.
176
+ *
177
+ * @param morceaux les fichiers des entrees, lus dans la carte de la construction ; `null` sans entrees
178
+ * declarees : la mesure reste alors la cloture de profondeur 2 (toutes les pages de route)
179
+ */
180
+ export function premierEcranDeLHote(sortie, depart, carte, morceaux) {
181
+ if (morceaux === null) return cloture(sortie, depart, carte, 2);
182
+ const ecran = cloture(sortie, depart, carte, 1);
183
+ for (const [chemin, taille] of cloture(sortie, morceaux, carte, 0)) {
184
+ if (!ecran.has(chemin)) ecran.set(chemin, taille);
185
+ }
186
+ return ecran;
187
+ }
188
+
189
+ /**
190
+ * Les fichiers des entrees du premier ecran (`starterWeb.premierEcran.entrees` : les sources, comme
191
+ * `starterWeb.ecran` d'une application autonome), lus dans la carte de la construction
192
+ * (`browser-stats.json`, que la cible esbuild d'`angular.json` ecrit avec `statsJson: true` : le
193
+ * builder de Native Federation refuse `--stats-json`). `null` sans entrees declarees. Une carte absente
194
+ * ou une entree introuvable est un defaut, jamais un repli silencieux sur l'ancienne mesure.
195
+ */
196
+ function entreesDuPremierEcran(projet, defauts) {
197
+ const entrees = projet.paquet.starterWeb?.premierEcran?.entrees;
198
+ if (!Array.isArray(entrees) || entrees.length === 0) return null;
199
+ const stats = lireJson(join(dirname(projet.sortie), 'browser-stats.json'));
200
+ if (stats === null) {
201
+ defauts.push(
202
+ 'starterWeb.premierEcran : browser-stats.json absent (poser statsJson: true sur la cible ' +
203
+ "esbuild d'angular.json, puis construire).",
204
+ );
205
+ return [];
206
+ }
207
+ const morceaux = [];
208
+ for (const entree of entrees) {
209
+ const morceau = morceauDeLEntree(stats, entree);
210
+ if (morceau === null) {
211
+ defauts.push(
212
+ `starterWeb.premierEcran : l'entree ${String(entree)} est introuvable dans browser-stats.json.`,
213
+ );
214
+ } else {
215
+ morceaux.push(morceau);
216
+ }
217
+ }
218
+ return morceaux;
219
+ }
220
+
161
221
  function budgetDeLHote(projet) {
162
222
  const index = join(projet.sortie, 'index.html');
163
223
  if (!existsSync(index)) {
164
224
  return { defauts: [`${projet.sortieRelative}/index.html absent : construire d'abord.`] };
165
225
  }
226
+ const defauts = [];
166
227
  const carte = lireJson(join(projet.sortie, 'importmap.json'))?.imports ?? {};
167
228
  const depart = departDeLaPage(readFileSync(index, 'utf8'));
168
229
  const initiale = totaux(cloture(projet.sortie, depart, carte, 1));
169
- const premierEcran = cloture(projet.sortie, depart, carte, 2);
230
+ const morceaux = entreesDuPremierEcran(projet, defauts);
231
+ const premierEcran = premierEcranDeLHote(projet.sortie, depart, carte, morceaux);
170
232
  const ecran = totaux(premierEcran);
171
233
  const details = [
172
234
  `charge initiale ${String(ko(initiale.brotli))} Ko br (${String(initiale.fichiers)} fichiers)`,
173
- `premier ecran reel ${String(ko(ecran.brotli))} Ko br (${String(ecran.fichiers)} fichiers)`,
235
+ `premier ecran reel ${String(ko(ecran.brotli))} Ko br (${String(ecran.fichiers)} fichiers, ` +
236
+ `${morceaux === null ? 'toutes les pages de route' : 'entrees declarees'})`,
174
237
  ...[...premierEcran.entries()]
175
238
  .sort((a, b) => b[1].brotli - a[1].brotli)
176
239
  .slice(0, 5)
177
240
  .map(([chemin, taille]) => ` ${String(ko(taille.brotli))} Ko br ${chemin}`),
178
241
  ];
179
- const defauts = [];
180
242
  if (ko(initiale.brotli) > BUDGETS_DE_L_HOTE.chargeInitialeKo) {
181
243
  defauts.push(
182
244
  `charge initiale : ${String(ko(initiale.brotli))} Ko br > ${String(BUDGETS_DE_L_HOTE.chargeInitialeKo)} Ko.`,
@@ -14,12 +14,19 @@
14
14
  *
15
15
  * La MARQUE de la SPA (niveau 2 bis : `marque.json`, `marque.ts` ou `marque.mjs` a la racine) est
16
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.
17
+ * tomber une paire sous son seuil rougit `verify`, paire nommee, avant la construction. Sa feuille
18
+ * versionnee, si la SPA la declare (`starterWeb.feuilleDeMarque`), doit etre celle que ce moteur
19
+ * engendre : une feuille perimee rougit aussi.
18
20
  */
19
21
  import { readFileSync } from 'node:fs';
20
22
  import { join } from 'node:path';
21
23
  import { fichiers, relatif } from '../commun.mjs';
22
- import { fichierDeMarque, jugerLaMarque } from '../marque.mjs';
24
+ import {
25
+ FICHIERS_DE_MARQUE,
26
+ deriveDeLaFeuille,
27
+ fichierDeMarque,
28
+ jugerLaMarque,
29
+ } from '../marque.mjs';
23
30
 
24
31
  /** Poses a l'execution par un composant du socle, donc absents des feuilles engendrees. */
25
32
  const POSES_A_L_EXECUTION = new Set([
@@ -110,11 +117,24 @@ export default {
110
117
  const { defauts, lues } = jugerLesJetons(feuilles, definis);
111
118
  let marque = '';
112
119
  const chemin = fichierDeMarque(projet.racine);
120
+ const versionnee = projet.paquet?.starterWeb?.feuilleDeMarque;
113
121
  if (chemin !== null) {
114
122
  const nom = relatif(projet.racine, chemin);
115
123
  const juge = await jugerLaMarque(projet.racine, chemin);
116
124
  for (const defaut of juge.defauts) defauts.push(`${nom} : ${defaut}`);
117
- if (juge.defauts.length === 0) marque = ` ; marque ${String(juge.valeurs.nom)} conforme`;
125
+ if (juge.defauts.length === 0) {
126
+ marque = ` ; marque ${String(juge.valeurs.nom)} conforme`;
127
+ if (versionnee !== undefined) {
128
+ const attendue = juge.moteur.feuilleDeMarque(juge.valeurs);
129
+ const derive = deriveDeLaFeuille(projet.racine, versionnee, juge.valeurs.nom, attendue);
130
+ if (derive === null) marque += `, ${String(versionnee)} a jour`;
131
+ else defauts.push(derive);
132
+ }
133
+ }
134
+ } else if (versionnee !== undefined) {
135
+ defauts.push(
136
+ `package.json : starterWeb.feuilleDeMarque sans fichier de marque (${FICHIERS_DE_MARQUE.join(', ')}).`,
137
+ );
118
138
  }
119
139
  return {
120
140
  defauts,
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * Pour tout fichier livre, cadre compris :
7
7
  * - eval, new Function, stockage du navigateur, lecture de cookie ;
8
- * - politiques Trusted Types comparees a la liste EXACTE du BFF
8
+ * - politiques Trusted Types comparees a la liste EXACTE du BFF, plus celles qu'une application declare
9
+ * (un remote dans `manifest.source.json#trustedTypes`, lot 270 ; une application autonome dans
10
+ * `package.json#starterWeb.trustedTypes`, 0.11.2)
9
11
  * (`platform.web.headers.csp.trusted-types-policies`), augmentee de celles que le remote
10
12
  * declare (`trustedTypes` de `manifest.source.json`, lot 270) ;
11
13
  * - cartes de source (et leurs variantes compressees), script dans un SVG ;
@@ -241,7 +243,8 @@ export function jugerUnFichierLivre(
241
243
  defauts.push(
242
244
  `${nom} : politique Trusted Types hors liste ("${politique}" ; admises : ` +
243
245
  `${[...POLITIQUES_ADMISES, ...declarees].join(', ')}). La declarer dans ` +
244
- 'manifest.source.json (trustedTypes) et a la CSP du BFF avant livraison.',
246
+ `${role === 'autonome' ? 'package.json (starterWeb.trustedTypes)' : 'manifest.source.json (trustedTypes)'} ` +
247
+ 'et a la CSP du BFF avant livraison.',
245
248
  );
246
249
  }
247
250
  }
@@ -271,15 +274,23 @@ export default {
271
274
  };
272
275
  }
273
276
  const { partages } = natureDesFichiers(lireJson(join(projet.sortie, 'remoteEntry.json')));
274
- // Les politiques que le remote declare (lot 270) ; le BFF retire celui qui en exige une que sa
275
- // CSP n'admet pas : l'etape le dit des la construction.
276
- const declarees = projet.role === 'remote' ? (projet.source?.trustedTypes ?? []) : [];
277
+ // Les politiques que l'application declare : un remote (lot 270), que le BFF retire s'il en exige une
278
+ // que sa CSP n'admet pas ; une application autonome (0.11.2), dont la page doit les porter nommement
279
+ // (la page d'ui-auth d'Aplomb et le widget de verification humaine). L'etape le dit des la construction.
280
+ const declarees =
281
+ projet.role === 'remote'
282
+ ? (projet.source?.trustedTypes ?? [])
283
+ : projet.role === 'autonome'
284
+ ? (projet.paquet.starterWeb?.trustedTypes ?? [])
285
+ : [];
277
286
  const avertissements = declarees
278
287
  .filter((politique) => !POLITIQUES_ADMISES.includes(politique))
279
- .map(
280
- (politique) =>
281
- `trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
282
- '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
288
+ .map((politique) =>
289
+ projet.role === 'autonome'
290
+ ? `trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
291
+ '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : la page de l application doit la porter nommement.'
292
+ : `trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
293
+ '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
283
294
  );
284
295
  const defauts = [];
285
296
  // Application autonome : la lecture de cookie du cadre, une seule fois dans tout le livrable.
package/src/marque.mjs CHANGED
@@ -7,9 +7,13 @@
7
7
  *
8
8
  * Fichier de marque : `marque.json`, ou un module `marque.ts` / `marque.mjs` qui exporte la table
9
9
  * (`export default`, ou `export const marque`). Forme : `ValeursDeMarque` du moteur (README des jetons).
10
+ *
11
+ * Une SPA qui VERSIONNE sa feuille (chargee par `angular.json`, relue a la revue) la declare dans
12
+ * `starterWeb.feuilleDeMarque` : l'etape `jetons` la confronte alors a celle que le moteur installe
13
+ * engendre, et la refuse si elle a derive (P09-01 d'Aplomb, qui la gardait par un script a lui).
10
14
  */
11
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
- import { join, resolve } from 'node:path';
16
+ import { basename, dirname, join, resolve } from 'node:path';
13
17
  import { pathToFileURL } from 'node:url';
14
18
  import { dossierInstalle } from './socle-installe.mjs';
15
19
 
@@ -76,3 +80,27 @@ export async function ecrireLaFeuilleDeMarque(racine, chemin, sortie) {
76
80
  writeFileSync(feuille, moteur.feuilleDeMarque(valeurs));
77
81
  return { valeurs, defauts, feuille };
78
82
  }
83
+
84
+ /**
85
+ * La feuille VERSIONNEE d'une SPA (`versionnee` : `starterWeb.feuilleDeMarque`, relatif a sa racine),
86
+ * confrontee a la feuille `attendue` que le moteur installe engendre de sa marque `nom` : `null` si elle
87
+ * est a jour, le defaut sinon. Les fins de ligne ne comptent pas (une copie de travail sous Windows les
88
+ * convertit).
89
+ */
90
+ export function deriveDeLaFeuille(racine, versionnee, nom, attendue) {
91
+ if (typeof versionnee !== 'string' || versionnee === '') {
92
+ return 'package.json : starterWeb.feuilleDeMarque, le chemin de la feuille versionnee, est attendu.';
93
+ }
94
+ const fichier = `starter-tokens-${nom}.css`;
95
+ if (basename(versionnee) !== fichier) {
96
+ return `starterWeb.feuilleDeMarque : ${versionnee}, la feuille de la marque ${nom} se nomme ${fichier}.`;
97
+ }
98
+ const regenerer = `starter-web jetons --sortie ${dirname(versionnee)}`;
99
+ const chemin = resolve(racine, versionnee);
100
+ if (!existsSync(chemin)) return `${versionnee} absente : ${regenerer}.`;
101
+ const lf = (texte) => texte.replace(/\r\n/g, '\n');
102
+ if (lf(readFileSync(chemin, 'utf8')) !== lf(attendue)) {
103
+ return `${versionnee} perimee : ${regenerer}, puis la relire et la commiter.`;
104
+ }
105
+ return null;
106
+ }