@starterlib/cli 0.10.13 → 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.
@@ -125,6 +125,33 @@ const EXCEPTIONS_DU_CADRE = [
125
125
  },
126
126
  ];
127
127
 
128
+ /**
129
+ * La lecture de cookie du CADRE dans une application AUTONOME (lot 240) : sans federation, le code
130
+ * d'Angular n'est pas dans un morceau partage mais dans `main`, et l'exception par nom de fichier ne
131
+ * s'applique pas. `DomAdapter.getCookie`, de cette forme exacte, est neutralise UNE fois dans tout le
132
+ * livrable ; toute autre lecture de cookie, n'importe ou, reste un defaut.
133
+ */
134
+ export const LECTURE_DE_COOKIE_DU_CADRE =
135
+ /getCookie\((\w+)\)\{return [\w$]+\(document\.cookie,\1\)\}/g;
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
+
128
155
  /** Les types d'un script EN LIGNE qui ne s'executent pas : des donnees (`esms-options`, cartes). */
129
156
  const SCRIPTS_DE_DONNEES = new Set([
130
157
  'esms-options',
@@ -164,13 +191,28 @@ function jugerUnePage(nom, contenu) {
164
191
 
165
192
  /**
166
193
  * Les defauts d'un fichier livre `nom` (relatif au dossier livre) de contenu `contenu`. `role` :
167
- * `remote` ou `hote` ; `partage` : le fichier est du code du cadre, partage par la federation.
194
+ * `remote`, `hote` ou `autonome` ; `partage` : le fichier est du code du cadre, partage par la
195
+ * federation ; `cadreNeutralise` : la lecture de cookie du cadre d'une application autonome a deja
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.
168
198
  */
169
199
  export function jugerUnFichierLivre(
170
200
  nom,
171
201
  contenu,
172
- { role = 'remote', partage = false, declarees = [] } = {},
202
+ {
203
+ role = 'remote',
204
+ partage = false,
205
+ declarees = [],
206
+ cadreNeutralise = false,
207
+ orchestrateurNeutralise = false,
208
+ } = {},
173
209
  ) {
210
+ if (cadreNeutralise) {
211
+ contenu = contenu.replace(LECTURE_DE_COOKIE_DU_CADRE, 'getCookie($1){return null}');
212
+ }
213
+ if (orchestrateurNeutralise) {
214
+ contenu = contenu.replace(STOCKAGES_DE_L_ORCHESTRATEUR, '$1=null;');
215
+ }
174
216
  // Une carte, ou sa variante compressee : `/x.js.map.br` se servirait aussi bien que l'original.
175
217
  if (/\.map(?:\.br|\.gz)?$/.test(nom)) {
176
218
  return [`${nom} : carte de source livree (sourceMap doit rester false en production).`];
@@ -217,7 +259,7 @@ export function jugerUnFichierLivre(
217
259
 
218
260
  export default {
219
261
  nom: 'securite',
220
- roles: ['remote', 'hote'],
262
+ roles: ['remote', 'hote', 'autonome'],
221
263
  executer(projet) {
222
264
  // Les variantes precompressees derivent des fichiers lus ; celles d'une carte sont signalees.
223
265
  const livres = fichiers(projet.sortie).filter(
@@ -240,21 +282,54 @@ export default {
240
282
  '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
241
283
  );
242
284
  const defauts = [];
285
+ // Application autonome : la lecture de cookie du cadre, une seule fois dans tout le livrable.
286
+ const autonome = projet.role === 'autonome';
287
+ let lecturesDuCadre = 0;
288
+ // Les strategies de stockage de l'orchestrateur : au plus une de chaque sorte dans le livrable.
289
+ const strategies = [];
243
290
  for (const chemin of livres) {
244
291
  const nom = relatif(projet.sortie, chemin);
245
292
  const contenu = /\.(?:m?js|html|svg|css)$/.test(nom) ? readFileSync(chemin, 'utf8') : '';
293
+ const duCadre =
294
+ autonome && /\.m?js$/.test(nom)
295
+ ? (contenu.match(LECTURE_DE_COOKIE_DU_CADRE)?.length ?? 0)
296
+ : 0;
297
+ lecturesDuCadre += duCadre;
298
+ const deLOrchestrateur = /\.m?js$/.test(nom) ? stockagesDeLOrchestrateur(contenu) : [];
299
+ strategies.push(...deLOrchestrateur);
246
300
  defauts.push(
247
301
  ...jugerUnFichierLivre(nom, contenu, {
248
302
  role: projet.role,
249
303
  partage: partages.has(nom),
250
304
  declarees,
305
+ cadreNeutralise: duCadre === 1,
306
+ orchestrateurNeutralise: deLOrchestrateur.length > 0,
251
307
  }),
252
308
  );
253
309
  }
310
+ if (lecturesDuCadre > 1) {
311
+ defauts.push(
312
+ `lecture de cookie du cadre : ${String(lecturesDuCadre)} occurrences (DomAdapter.getCookie) ; ` +
313
+ 'une seule est admise dans une application autonome.',
314
+ );
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
+ }
254
325
  return {
255
326
  defauts,
256
327
  avertissements,
257
- 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).`),
258
333
  };
259
334
  },
260
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
  };
@@ -23,7 +23,7 @@ import { retardEnTrains, trainDe } from '../trains.mjs';
23
23
  const FEDERATION = '@angular-architects/native-federation';
24
24
 
25
25
  /** Exiges dans toute SPA : ce que la matrice epingle et sans quoi rien ne se construit. */
26
- const EXIGES = [
26
+ export const EXIGES = [
27
27
  '@angular/core',
28
28
  '@angular/common',
29
29
  '@angular/compiler',
@@ -42,6 +42,12 @@ const EXIGES = [
42
42
  '@starterlib/util',
43
43
  ];
44
44
 
45
+ /**
46
+ * Ce qu'une application AUTONOME (lot 240 : l'ecran anonyme d'identity, ADR 0031 decision 4) n'a
47
+ * pas : elle n'est ni hote ni remote, et ne charge aucun moteur de federation.
48
+ */
49
+ const FEDERATION_ET_CHARGEUR = [FEDERATION, 'es-module-shims'];
50
+
45
51
  /** `22.1.7` donne `22.1` ; `null` pour ce qui n'est pas un numero. */
46
52
  function majeureMineure(version) {
47
53
  const trouve = /^(\d+)\.(\d+)\./.exec(version);
@@ -70,14 +76,23 @@ function trainDeReference(contexte) {
70
76
  }
71
77
 
72
78
  /** Les fautes et avertissements de `paquet` (le package.json) ; exporte pour les essais. */
73
- export function jugerLesVersions(paquet, { matrice, socle, reference }) {
79
+ export function jugerLesVersions(paquet, { matrice, socle, reference, role = 'remote' }) {
74
80
  const declarees = { ...paquet.dependencies, ...paquet.devDependencies };
75
81
  const defauts = [];
76
82
  const avertissements = [];
77
83
 
84
+ const autonome = role === 'autonome';
78
85
  for (const nom of EXIGES) {
86
+ if (autonome && FEDERATION_ET_CHARGEUR.includes(nom)) continue;
79
87
  if (declarees[nom] === undefined) defauts.push(`${nom} : absent de package.json.`);
80
88
  }
89
+ if (autonome) {
90
+ for (const nom of FEDERATION_ET_CHARGEUR) {
91
+ if (declarees[nom] !== undefined) {
92
+ defauts.push(`${nom} : une application autonome ne charge aucun moteur de federation.`);
93
+ }
94
+ }
95
+ }
81
96
 
82
97
  for (const [nom, attendue] of Object.entries(matrice.paquets)) {
83
98
  const declaree = declarees[nom];
@@ -165,7 +180,7 @@ export function jugerLesVersions(paquet, { matrice, socle, reference }) {
165
180
 
166
181
  export default {
167
182
  nom: 'versions',
168
- roles: ['remote', 'hote'],
183
+ roles: ['remote', 'hote', 'autonome'],
169
184
  executer(projet, contexte) {
170
185
  const matrice = matriceDuSocle();
171
186
  const socle = versionDuSocle();
@@ -174,6 +189,7 @@ export default {
174
189
  matrice,
175
190
  socle,
176
191
  reference,
192
+ role: projet.role,
177
193
  });
178
194
  return {
179
195
  defauts,
@@ -8,7 +8,7 @@ import { auditerLesDependances } from '../vulnerabilites.mjs';
8
8
 
9
9
  export default {
10
10
  nom: 'vulnerabilites',
11
- roles: ['remote', 'hote'],
11
+ roles: ['remote', 'hote', 'autonome'],
12
12
  executer(projet) {
13
13
  try {
14
14
  const { defauts, exceptes, composants } = auditerLesDependances(projet.racine);
package/src/index.mjs CHANGED
@@ -10,3 +10,11 @@ export { comparerLesTrains, retardEnTrains, trainDe } from './trains.mjs';
10
10
  export { precompresser } from './construction/precompresser.mjs';
11
11
  export { ecrireLeManifeste } from './construction/manifeste.mjs';
12
12
  export { poserLeSprite } from './construction/sprite.mjs';
13
+ // Le type des cles, engendre ici et par `@starterlib/schematics` (lot 285) : une seule ecriture.
14
+ export { cheminDesTypesDeCles, typesDeCles } from './construction/types-i18n.mjs';
15
+ export {
16
+ ecrireLesNavigateurs,
17
+ jugerLesNavigateurs,
18
+ navigateursDeLaSpa,
19
+ navigateursDuSocle,
20
+ } from './navigateurs.mjs';
package/src/lancer.mjs CHANGED
@@ -9,12 +9,16 @@ 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';
17
+ import { ecrireLesNavigateurs } from './navigateurs.mjs';
15
18
  import { versionDuSocle } from './matrice.mjs';
16
19
  import { ProjetIllisible, lireLeProjet } from './projet.mjs';
17
20
  import { ETAPES, etapesInconnues, verifier } from './verifier.mjs';
21
+ import { perimetresDuProjet } from './etapes/i18n.mjs';
18
22
  import { join, resolve } from 'node:path';
19
23
 
20
24
  const AIDE = `starter-web : la chaine d'une SPA du socle frontal (@starterlib/cli).
@@ -28,7 +32,12 @@ Commandes :
28
32
  precompresser ecrit les variantes .br et .gz (dossier livre, ou le dossier donne)
29
33
  i18n:types engendre le type des cles de chaque perimetre (src/app/i18n/cles.ts)
30
34
  simulations recopie les stubs WireMock du service pour l'hote de developpement
31
- (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>)
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
32
41
  etapes liste les etapes de verify
33
42
 
34
43
  Options de verify :
@@ -40,6 +49,30 @@ Options du projet (valeurs attendues, lues sinon dans ses fichiers) :
40
49
  --nom <nom> --prefixe <prefixe> --pleine-page | --pas-pleine-page
41
50
  `;
42
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
+
43
76
  /** Les arguments en `{ commande, positionnels, options }` ; leve sur une option inconnue. */
44
77
  export function lireLesArguments(argv) {
45
78
  const options = {};
@@ -82,6 +115,14 @@ export function lireLesArguments(argv) {
82
115
  options.port = valeur(indice, argument);
83
116
  indice += 1;
84
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;
85
126
  case '--configuration':
86
127
  options.configuration = valeur(indice, argument);
87
128
  indice += 1;
@@ -89,6 +130,9 @@ export function lireLesArguments(argv) {
89
130
  case '--pleine-page':
90
131
  options.pleinePage = true;
91
132
  break;
133
+ case '--scenarios':
134
+ options.scenarios = true;
135
+ break;
92
136
  case '--pas-pleine-page':
93
137
  options.pleinePage = false;
94
138
  break;
@@ -109,6 +153,25 @@ export function lireLesArguments(argv) {
109
153
  return { commande, positionnels: reste, options };
110
154
  }
111
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
+
112
175
  function precompresserEtDire(dossier, sortie) {
113
176
  const { resume, defauts } = juger(precompresser(dossier));
114
177
  sortie.info(`precompression : ${resume}`);
@@ -154,6 +217,19 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
154
217
  return 2;
155
218
  }
156
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
+
157
233
  // Un dossier donne se precompresse sans projet autour : une fixture reecrite, un dossier d'essai.
158
234
  if (commande === 'precompresser' && positionnels[0] !== undefined) {
159
235
  return precompresserEtDire(resolve(racine, positionnels[0]), sortie);
@@ -196,9 +272,15 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
196
272
  }
197
273
  case 'precompresser':
198
274
  return precompresserEtDire(projet.sortie, sortie);
275
+ case 'jetons':
276
+ return await marquer(projet.racine, options.marque, projet.sortie, sortie);
277
+ case 'navigateurs': {
278
+ const chemin = ecrireLesNavigateurs(projet.racine);
279
+ sortie.info(`navigateurs : ${relatif(projet.racine, chemin)} (plancher du socle)`);
280
+ return 0;
281
+ }
199
282
  case 'i18n:types': {
200
- const perimetres =
201
- projet.role === 'remote' ? (projet.source?.i18n?.perimetres ?? []) : ['socle'];
283
+ const perimetres = perimetresDuProjet(projet);
202
284
  const catalogues = Object.fromEntries(
203
285
  perimetres.map((perimetre) => [
204
286
  perimetre,
@@ -210,6 +292,7 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
210
292
  return 0;
211
293
  }
212
294
  case 'simulations': {
295
+ if (options.scenarios === true) return listerLesScenarios(projet, sortie);
213
296
  const resultat = await ecrireLesSimulations(projet);
214
297
  if (resultat === null) {
215
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
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * La liste des navigateurs PARTAGEE (lot 276) : le plancher du socle, ecrit UNE fois dans
3
+ * `docs/matrice-compatibilite.md` (section « Navigateurs », ADR web 0005), traduit en requetes
4
+ * browserslist que chaque SPA pose a sa racine (`.browserslistrc`) et que la construction d'Angular
5
+ * lit pour ses cibles. `starter-web navigateurs` l'ecrit ; l'etape `navigateurs` de `starter-web
6
+ * verify` refuse une liste qui s'en ecarte : une SPA ne vise ni plus bas que le socle (elle
7
+ * promettrait ce que les feuilles ne tiennent pas), ni plus haut (elle refuserait sans le dire un
8
+ * navigateur que le socle prend en charge).
9
+ *
10
+ * Edge suit Chrome ; la WebView d'Android suit Chromium et WKWebView le Safari du systeme (ADR
11
+ * 0043 du socle `api/starter`) : memes planchers.
12
+ */
13
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { lireJson } from './commun.mjs';
16
+ import { cheminDeLaMatrice } from './matrice.mjs';
17
+
18
+ /** « Plancher du socle (ADR web 0005) : **Chrome 123, Safari 17.5, Firefox 129** ». */
19
+ const LIGNE_DU_PLANCHER = /Plancher du socle[^*]*\*\*([^*]+)\*\*/;
20
+
21
+ /** Le plancher que la matrice ecrit : `{ chrome: '123', safari: '17.5', firefox: '129' }`. */
22
+ export function plancherDeLaMatrice(texte) {
23
+ const trouve = LIGNE_DU_PLANCHER.exec(texte);
24
+ if (trouve === null) {
25
+ throw new Error('matrice-compatibilite.md : aucune ligne « Plancher du socle ... **...** ».');
26
+ }
27
+ const plancher = {};
28
+ for (const morceau of (trouve[1] ?? '').split(',')) {
29
+ const [nom, version] = morceau.trim().split(/\s+/);
30
+ if (nom !== undefined && version !== undefined && /^\d+(\.\d+)?$/.test(version)) {
31
+ plancher[nom.toLowerCase()] = version;
32
+ }
33
+ }
34
+ for (const attendu of ['chrome', 'safari', 'firefox']) {
35
+ if (plancher[attendu] === undefined) {
36
+ throw new Error(`matrice-compatibilite.md : plancher sans ${attendu}.`);
37
+ }
38
+ }
39
+ return plancher;
40
+ }
41
+
42
+ /** Les requetes browserslist d'un plancher, dans un ordre fixe. */
43
+ export function requetesDuPlancher({ chrome, safari, firefox }) {
44
+ return [
45
+ `Chrome >= ${chrome}`,
46
+ `Edge >= ${chrome}`,
47
+ `Firefox >= ${firefox}`,
48
+ `Safari >= ${safari}`,
49
+ `iOS >= ${safari}`,
50
+ `ChromeAndroid >= ${chrome}`,
51
+ `Android >= ${chrome}`,
52
+ `FirefoxAndroid >= ${firefox}`,
53
+ ];
54
+ }
55
+
56
+ /** Les requetes du socle, lues dans la matrice publiee avec ce CLI. */
57
+ export function navigateursDuSocle() {
58
+ return requetesDuPlancher(plancherDeLaMatrice(readFileSync(cheminDeLaMatrice(), 'utf8')));
59
+ }
60
+
61
+ /** Une requete normalisee : espaces resserres, casse ignoree. */
62
+ const normaliser = (requete) => requete.trim().replace(/\s+/g, ' ').toLowerCase();
63
+
64
+ /** Les lignes utiles d'un `.browserslistrc` : sans commentaire ni ligne vide. */
65
+ function lignesDuFichier(texte) {
66
+ return texte
67
+ .split(/\r?\n/)
68
+ .map((ligne) => ligne.replace(/#.*$/, '').trim())
69
+ .filter((ligne) => ligne !== '')
70
+ .flatMap((ligne) => ligne.split(','))
71
+ .map((ligne) => ligne.trim())
72
+ .filter((ligne) => ligne !== '');
73
+ }
74
+
75
+ /**
76
+ * La liste que la SPA declare : `.browserslistrc` a sa racine, sinon `browserslist` de son
77
+ * `package.json`. `null` si elle n'en declare aucune (la construction prendrait alors la cible par
78
+ * defaut d'Angular, plus basse que le plancher du socle).
79
+ */
80
+ export function navigateursDeLaSpa(racine) {
81
+ const chemin = join(racine, '.browserslistrc');
82
+ if (existsSync(chemin)) {
83
+ return { origine: '.browserslistrc', requetes: lignesDuFichier(readFileSync(chemin, 'utf8')) };
84
+ }
85
+ const declares = lireJson(join(racine, 'package.json'))?.browserslist;
86
+ if (Array.isArray(declares)) return { origine: 'package.json', requetes: declares.map(String) };
87
+ if (typeof declares === 'string') {
88
+ return { origine: 'package.json', requetes: lignesDuFichier(declares) };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /** Les defauts d'une liste lue contre celle du socle (l'ordre ne compte pas). */
94
+ export function jugerLesNavigateurs(lus, attendus) {
95
+ if (lus === null) {
96
+ return [
97
+ 'aucune liste de navigateurs : poser celle du socle (starter-web navigateurs ecrit ' +
98
+ '.browserslistrc).',
99
+ ];
100
+ }
101
+ const voulus = new Map(attendus.map((requete) => [normaliser(requete), requete]));
102
+ const trouves = new Set(lus.requetes.map(normaliser));
103
+ const defauts = [];
104
+ for (const [cle, requete] of voulus) {
105
+ if (!trouves.has(cle)) defauts.push(`${lus.origine} : « ${requete} » manque.`);
106
+ }
107
+ for (const requete of lus.requetes) {
108
+ if (!voulus.has(normaliser(requete))) {
109
+ defauts.push(`${lus.origine} : « ${requete} » n'est pas dans la liste du socle.`);
110
+ }
111
+ }
112
+ return defauts;
113
+ }
114
+
115
+ /** Ecrit la liste du socle dans `.browserslistrc` ; rend son chemin. */
116
+ export function ecrireLesNavigateurs(racine, requetes = navigateursDuSocle()) {
117
+ const chemin = join(racine, '.browserslistrc');
118
+ writeFileSync(
119
+ chemin,
120
+ [
121
+ '# Liste du socle frontal (docs/matrice-compatibilite.md, « Navigateurs »), ecrite par',
122
+ '# starter-web navigateurs et verifiee par starter-web verify : ne pas la modifier a la main.',
123
+ ...requetes,
124
+ '',
125
+ ].join('\n'),
126
+ );
127
+ return chemin;
128
+ }
package/src/projet.mjs CHANGED
@@ -5,8 +5,9 @@
5
5
  * (`dist/<nom>/browser`, `/ui/<nom>/`), le prefixe de ses selecteurs et `pleinePage`. Le CLI les lit
6
6
  * la ou elles vivent deja :
7
7
  *
8
- * - le role : `package.json#starterWeb.role` (`hote` pour la coquille), sinon `remote` des qu'un
9
- * `manifest.source.json` existe ;
8
+ * - le role : `package.json#starterWeb.role` (`hote` pour la coquille, `autonome` pour une
9
+ * application servie seule, sans federation : l'ecran anonyme d'identity, lot 240), sinon `remote`
10
+ * des qu'un `manifest.source.json` existe ;
10
11
  * - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
11
12
  * - le prefixe : `prefix` du projet d'`angular.json`, ou `starterWeb.prefixe` ;
12
13
  * - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser`.
@@ -62,8 +63,10 @@ export function lireLeProjet(racine, options = {}) {
62
63
 
63
64
  const cheminSource = join(racine, 'manifest.source.json');
64
65
  const role = options.role ?? reglages.role ?? (existsSync(cheminSource) ? 'remote' : 'hote');
65
- if (role !== 'remote' && role !== 'hote') {
66
- throw new ProjetIllisible(`starterWeb.role : "remote" ou "hote", recu "${String(role)}".`);
66
+ if (role !== 'remote' && role !== 'hote' && role !== 'autonome') {
67
+ throw new ProjetIllisible(
68
+ `starterWeb.role : "remote", "hote" ou "autonome", recu "${String(role)}".`,
69
+ );
67
70
  }
68
71
  const source = role === 'remote' ? lireJson(cheminSource) : null;
69
72
  if (role === 'remote' && source === null) {