@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.
- package/README.md +48 -32
- package/matrice-compatibilite.md +35 -20
- package/package.json +1 -1
- package/src/construction/construire.mjs +13 -6
- package/src/construction/scenarios.mjs +155 -0
- package/src/construction/simulations.mjs +5 -2
- package/src/construction/sprite.mjs +9 -4
- package/src/etapes/autonome.mjs +131 -0
- package/src/etapes/budget.mjs +12 -8
- package/src/etapes/dependances.mjs +1 -1
- package/src/etapes/formulations.mjs +182 -0
- package/src/etapes/i18n.mjs +58 -8
- package/src/etapes/jetons.mjs +22 -3
- package/src/etapes/navigateurs.mjs +21 -0
- package/src/etapes/outillage.mjs +63 -5
- package/src/etapes/securite.mjs +79 -4
- package/src/etapes/simulations.mjs +20 -2
- package/src/etapes/versions.mjs +19 -3
- package/src/etapes/vulnerabilites.mjs +1 -1
- package/src/index.mjs +8 -0
- package/src/lancer.mjs +86 -3
- package/src/marque.mjs +78 -0
- package/src/navigateurs.mjs +128 -0
- package/src/projet.mjs +7 -4
- package/src/verifier.mjs +8 -4
package/src/etapes/securite.mjs
CHANGED
|
@@ -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 `
|
|
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
|
-
{
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
};
|
package/src/etapes/versions.mjs
CHANGED
|
@@ -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
|
|
9
|
-
* `
|
|
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(
|
|
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) {
|