@starterlib/cli 0.10.2 → 0.10.4

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.
@@ -90,5 +90,6 @@ partout d'un coup.
90
90
  `transloco` entrera ici quand le premier depot de SPA l'emploiera ; il declare
91
91
  une plage de pairs tres large (`>=16`), qui ne vaut pas compatibilite testee
92
92
  avec la 22. `orval` y est depuis l'ADR web 0008, a la version relevee le
93
- 2026-09-25 : la compatibilite avec la 22 se prouvera au premier client engendre
94
- (lot 175).
93
+ 2026-09-25 : sa compatibilite avec la 22 est prouvee depuis le lot 175 (2026-09-28), par les
94
+ clients engendres de la coquille, du temoin, de catalog et d'identity, construits et essayes
95
+ contre Angular 22.2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
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",
@@ -128,13 +128,18 @@ export function ecrireLeManifeste(projet) {
128
128
  `${prefixe}/i18n/${hacherLeCatalogue(join(projet.sortie, 'i18n'), nom, langue)}`;
129
129
  }
130
130
 
131
- const icones = join(projet.sortie, 'icones');
132
- const sprite = existsSync(icones)
133
- ? readdirSync(icones).find((fichier) => /^sprite-[A-Z0-9]{8}\.svg$/.test(fichier))
134
- : undefined;
135
- if (sprite === undefined) {
131
+ // Le sprite annonce est celui de la SOURCE, nomme par son empreinte (lot 194) : jamais le premier
132
+ // fichier venu d'un dossier ou deux sprites haches pouvaient coexister (le temoin y avait porte
133
+ // son sprite metier et une copie de celui du socle, et le manifeste pouvait annoncer l'autre).
134
+ const spriteSource = join(projet.racine, 'src', 'icones', 'sprite.svg');
135
+ if (!existsSync(spriteSource)) {
136
+ throw new Error('sprite metier absent : src/icones/sprite.svg.');
137
+ }
138
+ const sprite = `sprite-${empreinteCourte(readFileSync(spriteSource))}.svg`;
139
+ if (!existsSync(join(projet.sortie, 'icones', sprite))) {
136
140
  throw new Error(
137
- `sprite metier absent de ${projet.sortieRelative}/icones : starter-web sprite d'abord.`,
141
+ `${projet.sortieRelative}/icones/${sprite} absent (le sprite de src/icones/sprite.svg) : ` +
142
+ "starter-web sprite, puis construire, d'abord.",
138
143
  );
139
144
  }
140
145
 
@@ -152,6 +157,9 @@ export function ecrireLeManifeste(projet) {
152
157
  routes: source.routes,
153
158
  i18n: { perimetres: source.i18n?.perimetres, catalogues },
154
159
  sprite: `${prefixe}/icones/${sprite}`,
160
+ // Les politiques Trusted Types que le remote cree lui-meme (lot 270), comparees par le BFF a sa
161
+ // CSP : recopiees telles que declarees, absentes si le remote n'en cree aucune.
162
+ ...(source.trustedTypes === undefined ? {} : { trustedTypes: source.trustedTypes }),
155
163
  apiExigee: source.apiExigee,
156
164
  };
157
165
  writeFileSync(join(projet.sortie, 'manifest.json'), `${JSON.stringify(manifeste, null, 2)}\n`);
@@ -13,6 +13,7 @@
13
13
  * Une ancienne empreinte est retiree : deux sprites livres, et le manifeste pourrait annoncer le
14
14
  * mauvais.
15
15
  */
16
+ import { createHash } from 'node:crypto';
16
17
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
17
18
  import { join } from 'node:path';
18
19
  import { empreinteCourte } from '../commun.mjs';
@@ -47,6 +48,27 @@ function reglages(projet) {
47
48
  };
48
49
  }
49
50
 
51
+ /**
52
+ * L'hote livre le sprite du socle tel que `@starterlib/ui` l'a publie (lot 194) : le
53
+ * `catalogue.json` publie a cote en porte l'empreinte SHA-256. Un sprite recopie a la main, tronque
54
+ * ou d'une autre version que son catalogue arrete la construction, au lieu de livrer des icones que
55
+ * le catalogue ne decrit pas.
56
+ */
57
+ function verifierLeSpriteDuSocle(source, contenu) {
58
+ const chemin = join(source, '..', 'catalogue.json');
59
+ if (!existsSync(chemin)) {
60
+ throw new Error(`catalogue du sprite du socle absent : ${chemin}.`);
61
+ }
62
+ const annonce = JSON.parse(readFileSync(chemin, 'utf8')).sprite ?? {};
63
+ const lue = createHash('sha256').update(contenu).digest('hex');
64
+ if (annonce.sha256 !== lue) {
65
+ throw new Error(
66
+ `sprite du socle altere : empreinte ${empreinteCourte(contenu)}, le catalogue de ` +
67
+ `@starterlib/ui annonce ${String(annonce.empreinte)} (${chemin}).`,
68
+ );
69
+ }
70
+ }
71
+
50
72
  /** Pose le sprite hache et sa constante ; rend l'adresse servie. */
51
73
  export function poserLeSprite(projet) {
52
74
  const { source, dossier, adresse, genere, constante } = reglages(projet);
@@ -56,6 +78,7 @@ export function poserLeSprite(projet) {
56
78
  );
57
79
  }
58
80
  const contenu = readFileSync(source);
81
+ if (projet.role === 'hote') verifierLeSpriteDuSocle(source, contenu);
59
82
  const fichier = `sprite-${empreinteCourte(contenu)}.svg`;
60
83
  mkdirSync(dossier, { recursive: true });
61
84
  for (const existant of readdirSync(dossier)) {
@@ -34,16 +34,22 @@ export function typesDeCles(perimetres, catalogues) {
34
34
  for (const perimetre of perimetres) {
35
35
  const cles = Object.keys(catalogues[perimetre]?.fr ?? {}).sort();
36
36
  lignes.push('', `/** Les cles du perimetre ${perimetre}. */`);
37
- const enLigne = `export type ${nomDuType(perimetre)} = ${
38
- cles.length === 0 ? 'never' : cles.map((cle) => `'${cle}'`).join(' | ')
39
- };`;
40
- // La forme que Prettier (largeur 100) donnerait : une ligne si elle tient, une cle par ligne
41
- // sinon. Le fichier engendre passe ainsi `prettier --check` sans etre ecarte.
37
+ const union = cles.length === 0 ? 'never' : cles.map((cle) => `'${cle}'`).join(' | ');
38
+ const enLigne = `export type ${nomDuType(perimetre)} = ${union};`;
39
+ // La forme que Prettier (largeur 100) donne, dans ses trois cas : une ligne si elle tient ;
40
+ // sinon l'union sur sa propre ligne indentee, si elle y tient (0.10.3 : la 0.10.2 sautait ce
41
+ // cas, et un petit catalogue, le squelette de new-spa.sh pour un nom de cinq lettres, sortait
42
+ // en echec de `prettier --check`) ; sinon une cle par ligne. Le fichier engendre passe ainsi
43
+ // `prettier --check` sans etre ecarte (test/i18n.test.mjs le compare a Prettier).
42
44
  if (enLigne.length <= 100) {
43
45
  lignes.push(enLigne);
44
46
  continue;
45
47
  }
46
48
  lignes.push(`export type ${nomDuType(perimetre)} =`);
49
+ if (` ${union};`.length <= 100 || cles.length === 1) {
50
+ lignes.push(` ${union};`);
51
+ continue;
52
+ }
47
53
  cles.forEach((cle, indice) => {
48
54
  lignes.push(` | '${cle}'${indice === cles.length - 1 ? ';' : ''}`);
49
55
  });
@@ -100,17 +100,52 @@ export const test = {
100
100
  executer: (projet) => deleguer(projet, ['exec', 'ng', 'test', '--no-watch'], 'ng test conforme.'),
101
101
  };
102
102
 
103
+ /** Ou vit le client engendre (ADR web 0008). */
104
+ export const DOSSIER_DU_CLIENT_ENGENDRE = join('src', 'app', 'donnees', 'genere');
105
+
106
+ /** Un motif du contrat, tel qu'orval le recopie en commentaire du client engendre. */
107
+ const MOTIF_ENGENDRE = /@pattern\s+(\S.*?)\s*$/gm;
108
+
109
+ /**
110
+ * Les motifs du client engendre qui ne compilent pas en ECMA-262 (lot 175, report du lot 81) : un
111
+ * motif Java (`\p{Cntrl}`, quantificateur possessif, groupe atomique) recopie par le contrat. Il
112
+ * refuserait chaque saisie legitime, ou ne compilerait pas, le jour ou un formulaire ou un schema
113
+ * de validation le reprendrait. Lu avec le drapeau `u`, celui d'un motif HTML.
114
+ */
115
+ export function motifsNonPortables(racine) {
116
+ const defauts = [];
117
+ for (const chemin of fichiers(join(racine, DOSSIER_DU_CLIENT_ENGENDRE))) {
118
+ if (!chemin.endsWith('.ts')) continue;
119
+ for (const [, motif] of readFileSync(chemin, 'utf8').matchAll(MOTIF_ENGENDRE)) {
120
+ try {
121
+ new RegExp(motif, 'u');
122
+ } catch (erreur) {
123
+ defauts.push(
124
+ `${relatif(racine, chemin)} : motif ${motif} hors ECMA-262 (${erreur instanceof Error ? erreur.message : String(erreur)}) ; le corriger dans le contrat du service.`,
125
+ );
126
+ }
127
+ }
128
+ }
129
+ return defauts;
130
+ }
131
+
103
132
  /**
104
- * `contrat` : le client d'API engendre depuis le contrat du service, sans difference (lot 175).
105
- * Tant que la SPA n'a pas de script `contrat:check`, l'etape est sautee, et le dit.
133
+ * `contrat` : le client d'API engendre depuis le contrat du service, sans difference (lot 175), et
134
+ * sans motif qu'un navigateur ne sait pas lire. Tant que la SPA n'a pas de script `contrat:check`,
135
+ * l'etape est sautee, et le dit.
106
136
  */
107
137
  export const contrat = {
108
138
  nom: 'contrat',
109
139
  roles: ['remote', 'hote'],
110
140
  executer(projet) {
111
141
  if (projet.paquet.scripts?.['contrat:check'] === undefined) {
112
- return { sautee: 'aucun script contrat:check (client engendre : lot 175).' };
142
+ return { sautee: 'aucun script contrat:check : pas de client engendre (ADR web 0008).' };
113
143
  }
114
- return deleguer(projet, ['run', 'contrat:check'], 'client engendre sans difference.');
144
+ const resultat = deleguer(projet, ['run', 'contrat:check'], 'client engendre sans difference.');
145
+ if (resultat.defauts.length > 0) return resultat;
146
+ const motifs = motifsNonPortables(projet.racine);
147
+ return motifs.length === 0
148
+ ? { defauts: [], resume: 'client engendre sans difference, motifs ECMA-262.' }
149
+ : { defauts: motifs };
115
150
  },
116
151
  };
@@ -6,7 +6,8 @@
6
6
  * Pour tout fichier livre, cadre compris :
7
7
  * - eval, new Function, stockage du navigateur, lecture de cookie ;
8
8
  * - politiques Trusted Types comparees a la liste EXACTE du BFF
9
- * (`platform.web.headers.csp.trusted-types-policies`) ;
9
+ * (`platform.web.headers.csp.trusted-types-policies`), augmentee de celles que le remote
10
+ * declare (`trustedTypes` de `manifest.source.json`, lot 270) ;
10
11
  * - cartes de source (et leurs variantes compressees), script dans un SVG ;
11
12
  * - dans une page HTML : balise meta de politique de securite, script ou feuille EN LIGNE.
12
13
  *
@@ -28,7 +29,7 @@ import { join } from 'node:path';
28
29
  import { fichiers, lireJson, relatif } from '../commun.mjs';
29
30
  import { natureDesFichiers } from '../livraison.mjs';
30
31
 
31
- /** Les politiques que la CSP du BFF laisse creer (application.yml du BFF). */
32
+ /** Les politiques que la CSP du BFF laisse creer par defaut (application.yml du BFF). */
32
33
  export const POLITIQUES_ADMISES = ['angular', 'angular#components', 'es-module-shims', 'nfo'];
33
34
 
34
35
  const CREATION_DE_POLITIQUE = /trustedTypes\s*\.\s*createPolicy\(\s*['"]([^'"]*)['"]/g;
@@ -165,7 +166,11 @@ function jugerUnePage(nom, contenu) {
165
166
  * Les defauts d'un fichier livre `nom` (relatif au dossier livre) de contenu `contenu`. `role` :
166
167
  * `remote` ou `hote` ; `partage` : le fichier est du code du cadre, partage par la federation.
167
168
  */
168
- export function jugerUnFichierLivre(nom, contenu, { role = 'remote', partage = false } = {}) {
169
+ export function jugerUnFichierLivre(
170
+ nom,
171
+ contenu,
172
+ { role = 'remote', partage = false, declarees = [] } = {},
173
+ ) {
169
174
  // Une carte, ou sa variante compressee : `/x.js.map.br` se servirait aussi bien que l'original.
170
175
  if (/\.map(?:\.br|\.gz)?$/.test(nom)) {
171
176
  return [`${nom} : carte de source livree (sourceMap doit rester false en production).`];
@@ -190,10 +195,11 @@ export function jugerUnFichierLivre(nom, contenu, { role = 'remote', partage = f
190
195
  const duCadre = POLITIQUES_DU_CADRE.some(
191
196
  (exception) => exception.fichier.test(nom) && exception.politique === politique,
192
197
  );
193
- if (!POLITIQUES_ADMISES.includes(politique) && !duCadre) {
198
+ if (!POLITIQUES_ADMISES.includes(politique) && !declarees.includes(politique) && !duCadre) {
194
199
  defauts.push(
195
200
  `${nom} : politique Trusted Types hors liste ("${politique}" ; admises : ` +
196
- `${POLITIQUES_ADMISES.join(', ')}). La declarer au BFF avant livraison.`,
201
+ `${[...POLITIQUES_ADMISES, ...declarees].join(', ')}). La declarer dans ` +
202
+ 'manifest.source.json (trustedTypes) et a la CSP du BFF avant livraison.',
197
203
  );
198
204
  }
199
205
  }
@@ -223,16 +229,31 @@ export default {
223
229
  };
224
230
  }
225
231
  const { partages } = natureDesFichiers(lireJson(join(projet.sortie, 'remoteEntry.json')));
232
+ // Les politiques que le remote declare (lot 270) ; le BFF retire celui qui en exige une que sa
233
+ // CSP n'admet pas : l'etape le dit des la construction.
234
+ const declarees = projet.role === 'remote' ? (projet.source?.trustedTypes ?? []) : [];
235
+ const avertissements = declarees
236
+ .filter((politique) => !POLITIQUES_ADMISES.includes(politique))
237
+ .map(
238
+ (politique) =>
239
+ `trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
240
+ '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
241
+ );
226
242
  const defauts = [];
227
243
  for (const chemin of livres) {
228
244
  const nom = relatif(projet.sortie, chemin);
229
245
  const contenu = /\.(?:m?js|html|svg|css)$/.test(nom) ? readFileSync(chemin, 'utf8') : '';
230
246
  defauts.push(
231
- ...jugerUnFichierLivre(nom, contenu, { role: projet.role, partage: partages.has(nom) }),
247
+ ...jugerUnFichierLivre(nom, contenu, {
248
+ role: projet.role,
249
+ partage: partages.has(nom),
250
+ declarees,
251
+ }),
232
252
  );
233
253
  }
234
254
  return {
235
255
  defauts,
256
+ avertissements,
236
257
  resume: `${String(livres.length)} fichier(s) livre(s) dont ${String(partages.size)} du cadre partage, rien a signaler.`,
237
258
  };
238
259
  },
package/src/verifier.mjs CHANGED
@@ -75,12 +75,14 @@ export async function verifier(
75
75
 
76
76
  for (const [indice, etape] of retenues.entries()) {
77
77
  journal.info(`[${String(indice + 1)}/${String(retenues.length)}] ${etape.nom}`);
78
- if (etape.preparation === 'sprite' && !spritePose) {
79
- journal.info(` sprite : ${poserLeSprite(projet)}`);
80
- spritePose = true;
81
- }
82
78
  let resultat;
83
79
  try {
80
+ // Un sprite qui ne se pose pas (absent, altere) est un defaut de l'etape qui le lit, nomme
81
+ // comme les autres, et non une commande qui ne peut pas s'executer.
82
+ if (etape.preparation === 'sprite' && !spritePose) {
83
+ journal.info(` sprite : ${poserLeSprite(projet)}`);
84
+ spritePose = true;
85
+ }
84
86
  resultat = await etape.executer(projet, contexte);
85
87
  } catch (erreur) {
86
88
  resultat = { defauts: [erreur instanceof Error ? erreur.message : String(erreur)] };