@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 +29 -26
- package/package.json +1 -1
- package/src/construction/demarrer.mjs +52 -0
- package/src/construction/simulations.mjs +270 -0
- package/src/etapes/i18n.mjs +56 -0
- package/src/etapes/simulations.mjs +41 -0
- package/src/lancer.mjs +27 -0
- package/src/verifier.mjs +5 -2
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
|
|
37
|
-
| `starter-web
|
|
38
|
-
| `starter-web
|
|
39
|
-
| `starter-web
|
|
40
|
-
| `starter-web
|
|
41
|
-
| `starter-web
|
|
42
|
-
| `starter-web
|
|
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
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
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.
|
|
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
|
+
}
|
package/src/etapes/i18n.mjs
CHANGED
|
@@ -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 ->
|
|
6
|
-
* manifeste -> federation -> entree (hote) -> budget -> amorcage (hote) ->
|
|
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,
|