@starterlib/cli 0.10.4 → 0.10.5

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,38 +31,41 @@ Scripts d'une SPA :
31
31
 
32
32
  ## Commandes
33
33
 
34
- | Commande | Ce qu'elle fait |
35
- | --------------------------- | ------------------------------------------------------------------------------------------------ |
36
- | `starter-web verify` | Les etapes ci-dessous, dans l'ordre, arretees a la premiere qui echoue, dont le nom est ecrit |
37
- | `starter-web construire` | Sprite, `ng build`, prechargement de la police (hote), manifeste, precompression |
38
- | `starter-web sprite` | Pose le sprite hache (`public/`) et sa constante engendree (`sprite-genere.ts`) |
39
- | `starter-web manifeste` | Engendre `manifest.json` dans le dossier livre ; idempotent |
40
- | `starter-web precompresser` | Ecrit les variantes `.br` et `.gz` ; `starter-web precompresser <dossier>` pour un autre dossier |
41
- | `starter-web i18n:types` | Engendre `src/app/i18n/cles.ts`, le type des cles de chaque perimetre (`injecterTraduction<C>`) |
42
- | `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` ; `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 etapes` | Liste les etapes et les roles qu'elles concernent |
43
45
 
44
46
  Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peut pas s'executer
45
47
  (argument inconnu, projet illisible).
46
48
 
47
49
  ## Les etapes de `verify`
48
50
 
49
- | Etape | Role | Ce qu'elle garde |
50
- | ------------- | -------- | ----------------------------------------------------------------------------------------------------- |
51
- | `versions` | les deux | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
52
- | `dependances` | les deux | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
53
- | `jetons` | les deux | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
54
- | `format` | les deux | `prettier --check .` |
55
- | `lint` | les deux | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
56
- | `i18n` | les deux | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, types a jour |
57
- | `test` | les deux | `ng test --no-watch` |
58
- | `contrat` | les deux | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
59
- | `build` | les deux | `starter-web construire` |
60
- | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
61
- | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
62
- | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
63
- | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
64
- | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
65
- | `securite` | les deux | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
51
+ | Etape | Role | Ce qu'elle garde |
52
+ | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `versions` | les deux | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
54
+ | `dependances` | les deux | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
55
+ | `jetons` | les deux | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
56
+ | `format` | les deux | `prettier --check .` |
57
+ | `lint` | les deux | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
58
+ | `i18n` | les deux | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, aide de chaque page (`<perimetre>.aide.<page>`), types a jour |
59
+ | `test` | les deux | `ng test --no-watch` |
60
+ | `contrat` | les deux | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
61
+ | `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 |
62
+ | `build` | les deux | `starter-web construire` |
63
+ | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
64
+ | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
65
+ | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
66
+ | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
67
+ | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
68
+ | `securite` | les deux | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
66
69
 
67
70
  `--etapes a,b` n'en joue que certaines ; `--sauter a,b` en retire.
68
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
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,52 @@
1
+ /**
2
+ * `starter-web start` (lot 269) : l'hote de developpement d'une SPA, sans BFF ni coquille. Le sprite,
3
+ * puis (remote adosse a un service) les stubs WireMock recopies, puis `ng serve`, avec
4
+ * `STARTER_WEB_HOTE_DEV=1`.
5
+ *
6
+ * Pourquoi la variable : la federation ne partage que ce que les modules EXPOSES atteignent
7
+ * (`ignoreUnusedDeps`), et l'hote de developpement importe des points d'entree du socle que les
8
+ * routes d'un remote n'importent pas (la navigation, l'anglais du tronc). Sous cette variable,
9
+ * `exposesDeLHoteDeDeveloppement()` (`@starterlib/core/federation`) expose aussi l'amorce de l'hote :
10
+ * ce qu'elle importe est partage, donc resolu par la carte d'importation. Hors de `starter-web
11
+ * start`, rien n'est expose de plus : le livrable ne porte jamais l'hote.
12
+ */
13
+ import { spawnSync } from 'node:child_process';
14
+ import { relatif } from '../commun.mjs';
15
+ import { ecrireLesSimulations } from './simulations.mjs';
16
+ import { poserLeSprite } from './sprite.mjs';
17
+
18
+ /** Le nom de la variable ; le meme que celui de `@starterlib/core/federation` (essai du CLI). */
19
+ export const VARIABLE_DE_L_HOTE_DE_DEVELOPPEMENT = 'STARTER_WEB_HOTE_DEV';
20
+
21
+ /** Lance l'hote de developpement ; rend le code de sortie de `ng serve`. */
22
+ export async function demarrer(projet, { port, journal = console } = {}) {
23
+ journal.info(` sprite : ${poserLeSprite(projet)}`);
24
+ if (projet.role === 'remote') {
25
+ const simulations = await ecrireLesSimulations(projet);
26
+ if (simulations !== null) {
27
+ for (const avertissement of simulations.avertissements) journal.info(` ! ${avertissement}`);
28
+ if (simulations.defauts.length > 0) {
29
+ for (const defaut of simulations.defauts) journal.erreur(` - ${defaut}`);
30
+ return 1;
31
+ }
32
+ journal.info(
33
+ ` simulations : ${String(simulations.stubs.length)} stub(s) de ${relatif(projet.racine, simulations.racine)}`,
34
+ );
35
+ }
36
+ }
37
+ const resultat = spawnSync(
38
+ 'pnpm',
39
+ ['exec', 'ng', 'serve', ...(port === undefined ? [] : ['--port', port])],
40
+ {
41
+ cwd: projet.racine,
42
+ stdio: 'inherit',
43
+ shell: process.platform === 'win32',
44
+ env: { ...process.env, [VARIABLE_DE_L_HOTE_DE_DEVELOPPEMENT]: '1' },
45
+ },
46
+ );
47
+ if (resultat.error !== undefined) {
48
+ journal.erreur(` ng serve : ${resultat.error.message}`);
49
+ return 1;
50
+ }
51
+ return resultat.status ?? 1;
52
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * `starter-web simulations` (lot 269) : les stubs WireMock du service (`../src/mock` par defaut,
3
+ * `starterWeb.simulations` sinon), recopies avec leurs corps dans
4
+ * `src/hote-dev/simulations.engendre.json`, que l'hote de developpement du remote rejoue dans le
5
+ * navigateur (`@starterlib/testing/hote-dev`).
6
+ *
7
+ * UNE simulation : celle que le BFF sert en profil `mock`, et que `make mock-check` confronte au
8
+ * contrat OpenAPI du service. Les hotes de developpement en ecrivaient une seconde a la main, qui
9
+ * derivait (47 produits pagines d'un cote, une page fixe de l'autre) sans que rien le dise.
10
+ *
11
+ * Le fichier est versionne (un stub ajoute se lit a la revue) et formate par le Prettier de la SPA :
12
+ * il passe `prettier --check` sans etre ecarte. L'etape `simulations` de `verify` le veut a jour.
13
+ *
14
+ * Un stub hors du sous-ensemble que l'hote rejoue (scenario, condition inconnue, chemin JSON hors
15
+ * grammaire, faute ou mandataire) est ECARTE du fichier, et nomme en avertissement : le BFF le sert
16
+ * toujours, l'hote de developpement non. Un fichier illisible ou un corps introuvable est un DEFAUT :
17
+ * WireMock lui-meme ne le servirait pas.
18
+ */
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
20
+ import { createRequire } from 'node:module';
21
+ import { dirname, join, relative, resolve, sep } from 'node:path';
22
+ import { pathToFileURL } from 'node:url';
23
+ import { fichiers } from '../commun.mjs';
24
+
25
+ /** Ou vit le fichier engendre, depuis la racine de la SPA. */
26
+ export const CHEMIN_DES_SIMULATIONS = join('src', 'hote-dev', 'simulations.engendre.json');
27
+
28
+ /** La racine WireMock par defaut : celle du service, voisine de son interface. */
29
+ const RACINE_PAR_DEFAUT = join('..', 'src', 'mock');
30
+
31
+ const CLES_DU_STUB = new Set([
32
+ 'name',
33
+ 'id',
34
+ 'uuid',
35
+ 'priority',
36
+ 'request',
37
+ 'response',
38
+ 'metadata',
39
+ 'persistent',
40
+ ]);
41
+ const CLES_DE_REQUETE = new Set([
42
+ 'method',
43
+ 'url',
44
+ 'urlPattern',
45
+ 'urlPath',
46
+ 'urlPathPattern',
47
+ 'queryParameters',
48
+ 'headers',
49
+ 'bodyPatterns',
50
+ ]);
51
+ const CLES_DE_CONDITION = new Set(['equalTo', 'contains', 'matches', 'absent', 'caseInsensitive']);
52
+ const CLES_DE_CORPS = new Set(['equalTo', 'contains', 'matches', 'equalToJson', 'matchesJsonPath']);
53
+ const CLES_DE_REPONSE = new Set([
54
+ 'status',
55
+ 'headers',
56
+ 'jsonBody',
57
+ 'body',
58
+ 'bodyFileName',
59
+ 'fixedDelayMilliseconds',
60
+ 'transformers',
61
+ ]);
62
+
63
+ const NOM = /^[A-Za-z_$][\w$-]*/;
64
+ const FILTRE =
65
+ /^\[\?\(@((?:\.[A-Za-z_$][\w$-]*)+)\s*(?:(==|!=|<=|>=|<|>)\s*('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?|true|false|null)\s*)?\)\]/;
66
+
67
+ /**
68
+ * `null` si l'hote de developpement comprend le chemin JSON, sinon la raison. Meme grammaire que
69
+ * `lireUnCheminJson` (`@starterlib/testing/hote-dev`), eprouvee sur le meme corpus.
70
+ */
71
+ export function cheminJsonIncompris(expression) {
72
+ if (typeof expression !== 'string' || !expression.startsWith('$')) {
73
+ return `chemin JSON "${String(expression)}" : il commence par $.`;
74
+ }
75
+ let reste = expression.slice(1);
76
+ while (reste !== '') {
77
+ if (reste.startsWith('..'))
78
+ return `chemin JSON "${expression}" : la descente .. n'est pas comprise.`;
79
+ if (reste.startsWith('.*')) {
80
+ reste = reste.slice(2);
81
+ continue;
82
+ }
83
+ if (reste.startsWith('.')) {
84
+ const nom = NOM.exec(reste.slice(1));
85
+ if (nom === null || reste.slice(1 + nom[0].length).startsWith('(')) {
86
+ return `chemin JSON "${expression}" : pas illisible apres ".".`;
87
+ }
88
+ reste = reste.slice(1 + nom[0].length);
89
+ continue;
90
+ }
91
+ const pas =
92
+ /^\[(?:'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")\]/.exec(reste) ??
93
+ /^\[\d+\]/.exec(reste) ??
94
+ /^\[\*\]/.exec(reste) ??
95
+ FILTRE.exec(reste);
96
+ if (pas === null) return `chemin JSON "${expression}" : "${reste}" n'est pas compris.`;
97
+ reste = reste.slice(pas[0].length);
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /** La racine WireMock de la SPA (`mappings/` dedans), ou `null` si elle n'en a pas. */
103
+ export function racineDesStubs(projet) {
104
+ const reglage = projet.paquet?.starterWeb?.simulations;
105
+ const racine = resolve(projet.racine, reglage ?? RACINE_PAR_DEFAUT);
106
+ if (existsSync(join(racine, 'mappings'))) return racine;
107
+ if (reglage !== undefined) {
108
+ throw new Error(`starterWeb.simulations : "${reglage}" ne contient pas de dossier mappings/.`);
109
+ }
110
+ return null;
111
+ }
112
+
113
+ /** Les raisons pour lesquelles l'hote ne rejouerait pas ce stub (vide : il le rejoue). */
114
+ function horsDuSousEnsemble(stub) {
115
+ const raisons = [];
116
+ const inconnues = (objet, admises, ou) => {
117
+ for (const cle of Object.keys(objet ?? {})) {
118
+ if (!admises.has(cle)) raisons.push(`${ou}${cle}`);
119
+ }
120
+ };
121
+ inconnues(stub, CLES_DU_STUB, '');
122
+ inconnues(stub.request, CLES_DE_REQUETE, 'request.');
123
+ inconnues(stub.response, CLES_DE_REPONSE, 'response.');
124
+ for (const famille of ['queryParameters', 'headers']) {
125
+ for (const [nom, condition] of Object.entries(stub.request?.[famille] ?? {})) {
126
+ inconnues(condition, CLES_DE_CONDITION, `request.${famille}.${nom}.`);
127
+ }
128
+ }
129
+ for (const [indice, motif] of (stub.request?.bodyPatterns ?? []).entries()) {
130
+ inconnues(motif, CLES_DE_CORPS, `request.bodyPatterns[${String(indice)}].`);
131
+ if (motif.matchesJsonPath !== undefined) {
132
+ const raison = cheminJsonIncompris(motif.matchesJsonPath);
133
+ if (raison !== null) raisons.push(raison);
134
+ }
135
+ }
136
+ for (const motif of [stub.request?.urlPattern, stub.request?.urlPathPattern]) {
137
+ if (motif === undefined) continue;
138
+ try {
139
+ new RegExp(`^(?:${motif})$`);
140
+ } catch {
141
+ raisons.push(`motif "${motif}" hors ECMA-262`);
142
+ }
143
+ }
144
+ return raisons;
145
+ }
146
+
147
+ /** Les stubs d'un fichier de `mappings/` (un stub, ou `{ mappings: [...] }`). */
148
+ function stubsDuFichier(chemin) {
149
+ const contenu = JSON.parse(readFileSync(chemin, 'utf8'));
150
+ if (Array.isArray(contenu?.mappings)) return contenu.mappings;
151
+ if (contenu?.request !== undefined) return [contenu];
152
+ throw new Error('ni stub (request), ni liste (mappings)');
153
+ }
154
+
155
+ /**
156
+ * Lit les stubs de `racine` : `{ stubs, avertissements, defauts }`, les stubs dans l'ordre des
157
+ * fichiers (celui dans lequel WireMock les charge : a priorite egale, le dernier l'emporte), leurs
158
+ * corps de `__files/` en ligne.
159
+ */
160
+ export function lireLesStubs(racine) {
161
+ const stubs = [];
162
+ const avertissements = [];
163
+ const defauts = [];
164
+ const mappings = join(racine, 'mappings');
165
+ const chemins = fichiers(mappings)
166
+ .filter((chemin) => chemin.endsWith('.json'))
167
+ .map((chemin) => relative(mappings, chemin).split(sep).join('/'))
168
+ .sort();
169
+ for (const nomDeFichier of chemins) {
170
+ let lus;
171
+ try {
172
+ lus = stubsDuFichier(join(mappings, nomDeFichier));
173
+ } catch (erreur) {
174
+ defauts.push(
175
+ `mappings/${nomDeFichier} illisible : ${erreur instanceof Error ? erreur.message : String(erreur)}.`,
176
+ );
177
+ continue;
178
+ }
179
+ for (const stub of lus) {
180
+ const nom = stub.name ?? nomDeFichier;
181
+ const raisons = horsDuSousEnsemble(stub);
182
+ if (raisons.length > 0) {
183
+ avertissements.push(
184
+ `stub "${nom}" (mappings/${nomDeFichier}) ecarte de l'hote : ${raisons.join(', ')}.`,
185
+ );
186
+ continue;
187
+ }
188
+ const { bodyFileName, transformers, ...reponse } = stub.response ?? {};
189
+ if ((transformers ?? []).length > 0) {
190
+ avertissements.push(
191
+ `stub "${nom}" : gabarit de reponse (${transformers.join(', ')}) servi tel quel par l'hote.`,
192
+ );
193
+ }
194
+ if (bodyFileName !== undefined) {
195
+ const cheminDuCorps = join(racine, '__files', bodyFileName);
196
+ if (!existsSync(cheminDuCorps)) {
197
+ defauts.push(
198
+ `stub "${nom}" (mappings/${nomDeFichier}) : __files/${bodyFileName} introuvable.`,
199
+ );
200
+ continue;
201
+ }
202
+ const texte = readFileSync(cheminDuCorps, 'utf8');
203
+ try {
204
+ if (!bodyFileName.endsWith('.json')) throw new Error('texte');
205
+ reponse.jsonBody = JSON.parse(texte);
206
+ } catch {
207
+ reponse.body = texte;
208
+ }
209
+ }
210
+ stubs.push({
211
+ name: nom,
212
+ ...(stub.priority === undefined ? {} : { priority: stub.priority }),
213
+ request: stub.request,
214
+ response: reponse,
215
+ });
216
+ }
217
+ }
218
+ return { stubs, avertissements, defauts };
219
+ }
220
+
221
+ /** Le Prettier de la SPA (celui de son `format`), ou `null`. */
222
+ async function prettierDe(racine) {
223
+ for (const depuis of [join(racine, 'package.json'), import.meta.url]) {
224
+ try {
225
+ const chemin = createRequire(depuis).resolve('prettier');
226
+ const module = await import(pathToFileURL(chemin).href);
227
+ // Resolu par `require`, Prettier est son entree CommonJS : ses fonctions sont sous `default`.
228
+ return typeof module.resolveConfig === 'function' ? module : module.default;
229
+ } catch {
230
+ // Le suivant : la SPA d'abord, puis le CLI (essais du socle).
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Le texte du fichier engendre pour `projet` : `{ texte, stubs, avertissements, defauts, racine }`,
238
+ * ou `null` si la SPA n'a pas de simulateur WireMock.
239
+ */
240
+ export async function engendrerLesSimulations(projet) {
241
+ const racine = racineDesStubs(projet);
242
+ if (racine === null) return null;
243
+ const { stubs, avertissements, defauts } = lireLesStubs(racine);
244
+ const contenu = {
245
+ $comment:
246
+ 'Engendre par starter-web simulations (@starterlib/cli) depuis les stubs WireMock du service : ne pas modifier a la main.',
247
+ source: relative(projet.racine, racine).split(sep).join('/'),
248
+ stubs,
249
+ };
250
+ let texte = `${JSON.stringify(contenu, null, 2)}\n`;
251
+ const cible = join(projet.racine, CHEMIN_DES_SIMULATIONS);
252
+ const prettier = await prettierDe(projet.racine);
253
+ if (prettier === null) {
254
+ avertissements.push('prettier introuvable depuis la SPA : fichier engendre non formate.');
255
+ } else {
256
+ const options = (await prettier.resolveConfig(cible)) ?? {};
257
+ texte = await prettier.format(texte, { ...options, filepath: cible });
258
+ }
259
+ return { texte, stubs, avertissements, defauts, racine };
260
+ }
261
+
262
+ /** Ecrit le fichier ; rend le resultat d'`engendrerLesSimulations` (ou `null`). */
263
+ export async function ecrireLesSimulations(projet) {
264
+ const resultat = await engendrerLesSimulations(projet);
265
+ if (resultat === null || resultat.defauts.length > 0) return resultat;
266
+ const cible = join(projet.racine, CHEMIN_DES_SIMULATIONS);
267
+ mkdirSync(dirname(cible), { recursive: true });
268
+ writeFileSync(cible, resultat.texte);
269
+ return resultat;
270
+ }
@@ -12,6 +12,9 @@
12
12
  * - un message porte les MEMES arguments en fr et en en : un `{nom}` traduit `{name}` reste un trou ;
13
13
  * - chaque cle que le code appelle (`t('catalog.x')`) et chaque titre de route du manifeste existent
14
14
  * au catalogue : une cle absente s'affiche telle quelle, a l'ecran ;
15
+ * - chaque route du manifeste porte son aide contextuelle (lot 280) : son titre suit la convention
16
+ * `<perimetre>.pages.<page>`, et la cle soeur `<perimetre>.aide.<page>` existe (le texte de
17
+ * l'aide que `st-page-header` pose a cote du titre) ; une aide sans page est une faute de frappe ;
15
18
  * - l'hote porte les cles que `@starterlib/core` traduit par son catalogue
16
19
  * (`schemas/cles-exigees-du-socle.json`, constat A0837) ;
17
20
  * - le type des cles engendre (`src/app/i18n/cles.ts`, `starter-web i18n:types`) est a jour : une cle
@@ -202,6 +205,56 @@ export function clesAppelees(racine) {
202
205
  return appelees;
203
206
  }
204
207
 
208
+ /** Un titre de page selon la convention : `catalog.pages.produits` -> perimetre et page. */
209
+ const TITRE_DE_PAGE = /^([a-z][\w-]*)\.pages\.([\w-]+(?:\.[\w-]+)*)$/;
210
+
211
+ /**
212
+ * La cle d'aide contextuelle d'un titre de page (lot 280) : `catalog.pages.produits` ->
213
+ * `catalog.aide.produits` ; `null` pour un titre hors convention.
214
+ */
215
+ export function cleDAide(titre) {
216
+ const morceaux = TITRE_DE_PAGE.exec(titre);
217
+ return morceaux === null ? null : `${morceaux[1]}.aide.${morceaux[2]}`;
218
+ }
219
+
220
+ /**
221
+ * Les defauts de l'aide contextuelle (lot 280), `catalogues` etant `{ perimetre: { fr, en } }` et
222
+ * `routes` celles du manifeste : chaque route titree `<perimetre>.pages.<page>` a sa cle
223
+ * `<perimetre>.aide.<page>` ; chaque cle `<perimetre>.aide.<page>` a sa page. L'anglais suit par
224
+ * l'egalite des jeux de cles, deja jugee.
225
+ */
226
+ export function jugerLesAides(catalogues, routes) {
227
+ const defauts = [];
228
+ for (const route of routes) {
229
+ if (typeof route.titre !== 'string') continue;
230
+ const catalogue = catalogues[route.titre.split('.')[0]]?.fr;
231
+ if (catalogue === undefined || catalogue === null) continue;
232
+ const cle = cleDAide(route.titre);
233
+ if (cle === null) {
234
+ defauts.push(
235
+ `manifest.source.json : titre "${route.titre}" hors de la convention <perimetre>.pages.<page> : l'aide de la page ne se deduit pas.`,
236
+ );
237
+ } else if (!Object.hasOwn(catalogue, cle)) {
238
+ defauts.push(
239
+ `manifest.source.json : route "${String(route.prefixe)}", aide "${cle}" absente du catalogue (aide contextuelle de st-page-header).`,
240
+ );
241
+ }
242
+ }
243
+ for (const [perimetre, parLangue] of Object.entries(catalogues)) {
244
+ const catalogue = parLangue?.fr;
245
+ if (catalogue === undefined || catalogue === null) continue;
246
+ const prefixe = `${perimetre}.aide.`;
247
+ for (const cle of Object.keys(catalogue)) {
248
+ if (!cle.startsWith(prefixe)) continue;
249
+ const page = `${perimetre}.pages.${cle.slice(prefixe.length)}`;
250
+ if (!Object.hasOwn(catalogue, page)) {
251
+ defauts.push(`${perimetre}.fr.json : aide "${cle}" sans page "${page}".`);
252
+ }
253
+ }
254
+ }
255
+ return defauts;
256
+ }
257
+
205
258
  function lire(chemin) {
206
259
  if (!existsSync(chemin)) return null;
207
260
  try {
@@ -249,6 +302,9 @@ export default {
249
302
  }
250
303
  }
251
304
 
305
+ // L'aide contextuelle de chaque page du manifeste, et aucune aide sans page (lot 280).
306
+ defauts.push(...jugerLesAides(catalogues, projet.source?.routes ?? []));
307
+
252
308
  // L'hote porte les cles que le noyau traduit par son catalogue.
253
309
  if (projet.role === 'hote') {
254
310
  const dossier = dossierInstalle(projet.racine, '@starterlib/core');
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Etape `simulations` (lot 269) : les stubs WireMock du service, tels que l'hote de developpement
3
+ * du remote les rejoue (`src/hote-dev/simulations.engendre.json`), sont A JOUR. Sautee pour un
4
+ * remote sans simulateur (le temoin : aucun service derriere lui).
5
+ *
6
+ * Un stub que l'hote ne sait pas rejouer est nomme en avertissement (le BFF en profil `mock` le sert
7
+ * toujours) ; un fichier de stub illisible ou un corps introuvable est un defaut.
8
+ */
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { relatif } from '../commun.mjs';
12
+ import { CHEMIN_DES_SIMULATIONS, engendrerLesSimulations } from '../construction/simulations.mjs';
13
+
14
+ export default {
15
+ nom: 'simulations',
16
+ roles: ['remote'],
17
+ async executer(projet) {
18
+ const resultat = await engendrerLesSimulations(projet);
19
+ if (resultat === null) {
20
+ return {
21
+ sautee:
22
+ "aucun simulateur WireMock (../src/mock, ou starterWeb.simulations) : l'hote de developpement ne rejoue que ses simulations ecrites.",
23
+ };
24
+ }
25
+ const { texte, stubs, avertissements, defauts, racine } = resultat;
26
+ if (defauts.length > 0) return { defauts, avertissements };
27
+ const cible = join(projet.racine, CHEMIN_DES_SIMULATIONS);
28
+ const chemin = relatif(projet.racine, cible);
29
+ if (!existsSync(cible)) {
30
+ return { defauts: [`${chemin} absent : starter-web simulations.`], avertissements };
31
+ }
32
+ if (readFileSync(cible, 'utf8') !== texte) {
33
+ return { defauts: [`${chemin} perime : starter-web simulations.`], avertissements };
34
+ }
35
+ return {
36
+ defauts: [],
37
+ avertissements,
38
+ resume: `${String(stubs.length)} stub(s) de ${relatif(projet.racine, racine)} rejoues par l'hote de developpement, a jour.`,
39
+ };
40
+ },
41
+ };
package/src/lancer.mjs CHANGED
@@ -5,9 +5,11 @@
5
5
  * pas s'executer (argument inconnu, projet illisible).
6
6
  */
7
7
  import { construire } from './construction/construire.mjs';
8
+ import { demarrer } from './construction/demarrer.mjs';
8
9
  import { ecrireLeManifeste } from './construction/manifeste.mjs';
9
10
  import { juger, precompresser } from './construction/precompresser.mjs';
10
11
  import { poserLeSprite } from './construction/sprite.mjs';
12
+ import { ecrireLesSimulations } from './construction/simulations.mjs';
11
13
  import { ecrireLesTypesDeCles } from './construction/types-i18n.mjs';
12
14
  import { lireJson, relatif } from './commun.mjs';
13
15
  import { versionDuSocle } from './matrice.mjs';
@@ -19,11 +21,14 @@ const AIDE = `starter-web : la chaine d'une SPA du socle frontal (@starterlib/cl
19
21
 
20
22
  Commandes :
21
23
  verify les etapes, dans l'ordre, arretees a la premiere qui echoue
24
+ start l'hote de developpement : sprite, simulations, ng serve (--port N)
22
25
  construire sprite, ng build, manifeste, precompression (--configuration development)
23
26
  sprite pose le sprite hache et sa constante
24
27
  manifeste engendre manifest.json dans le dossier livre
25
28
  precompresser ecrit les variantes .br et .gz (dossier livre, ou le dossier donne)
26
29
  i18n:types engendre le type des cles de chaque perimetre (src/app/i18n/cles.ts)
30
+ simulations recopie les stubs WireMock du service pour l'hote de developpement
31
+ (src/hote-dev/simulations.engendre.json)
27
32
  etapes liste les etapes de verify
28
33
 
29
34
  Options de verify :
@@ -73,6 +78,10 @@ export function lireLesArguments(argv) {
73
78
  options.prefixe = valeur(indice, argument);
74
79
  indice += 1;
75
80
  break;
81
+ case '--port':
82
+ options.port = valeur(indice, argument);
83
+ indice += 1;
84
+ break;
76
85
  case '--configuration':
77
86
  options.configuration = valeur(indice, argument);
78
87
  indice += 1;
@@ -171,6 +180,8 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
171
180
  );
172
181
  return await verifier(projet, { ...options, journal: sortie });
173
182
  }
183
+ case 'start':
184
+ return await demarrer(projet, { port: options.port, journal: sortie });
174
185
  case 'construire':
175
186
  return construire(projet, { configuration: options.configuration, journal: sortie });
176
187
  case 'sprite':
@@ -198,6 +209,22 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
198
209
  sortie.info(`i18n:types : ${relatif(projet.racine, chemin)} (${perimetres.join(', ')})`);
199
210
  return 0;
200
211
  }
212
+ case 'simulations': {
213
+ const resultat = await ecrireLesSimulations(projet);
214
+ if (resultat === null) {
215
+ sortie.info(
216
+ 'simulations : aucun simulateur WireMock (../src/mock, ou starterWeb.simulations).',
217
+ );
218
+ return 0;
219
+ }
220
+ for (const avertissement of resultat.avertissements) sortie.info(` ! ${avertissement}`);
221
+ for (const defaut of resultat.defauts) sortie.erreur(` - ${defaut}`);
222
+ if (resultat.defauts.length > 0) return 1;
223
+ sortie.info(
224
+ `simulations : ${String(resultat.stubs.length)} stub(s) de ${relatif(projet.racine, resultat.racine)}`,
225
+ );
226
+ return 0;
227
+ }
201
228
  default:
202
229
  sortie.erreur(`starter-web : commande inconnue : ${commande}\n\n${AIDE}`);
203
230
  return 2;
package/src/verifier.mjs CHANGED
@@ -2,8 +2,9 @@
2
2
  * `starter-web verify` : les etapes d'une SPA, dans l'ordre du plan SPA §3, arretees a la premiere
3
3
  * qui echoue — et qui la NOMME : un « Command failed » ne dit pas quoi corriger.
4
4
  *
5
- * versions -> dependances -> jetons -> format -> lint -> i18n -> test -> contrat -> build ->
6
- * manifeste -> federation -> entree (hote) -> budget -> amorcage (hote) -> securite.
5
+ * versions -> dependances -> jetons -> format -> lint -> i18n -> test -> contrat -> simulations
6
+ * (remote) -> build -> manifeste -> federation -> entree (hote) -> budget -> amorcage (hote) ->
7
+ * securite.
7
8
  *
8
9
  * Le sprite est pose une fois, avant la premiere etape qui lit le code (lint, test, build).
9
10
  */
@@ -18,6 +19,7 @@ import jetons from './etapes/jetons.mjs';
18
19
  import manifeste from './etapes/manifeste.mjs';
19
20
  import { contrat, format, lint, test } from './etapes/outillage.mjs';
20
21
  import securite from './etapes/securite.mjs';
22
+ import simulations from './etapes/simulations.mjs';
21
23
  import versions from './etapes/versions.mjs';
22
24
 
23
25
  const build = {
@@ -42,6 +44,7 @@ export const ETAPES = [
42
44
  i18n,
43
45
  test,
44
46
  contrat,
47
+ simulations,
45
48
  build,
46
49
  manifeste,
47
50
  federation,