@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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Etape `autonome` (lot 240) : ce qu'une application AUTONOME doit tenir, et que les etapes d'un hote
|
|
3
|
+
* ou d'un remote ne voient pas. Une application autonome est servie seule, sans federation : l'ecran
|
|
4
|
+
* anonyme d'identity (`ui-auth`, ADR 0031 decision 4), que le BFF rend sur `/auth/**`.
|
|
5
|
+
*
|
|
6
|
+
* 1. AUCUNE FEDERATION LIVREE : ni `remoteEntry.json`, ni carte d'importation, ni `es-module-shims`,
|
|
7
|
+
* ni le moteur de Native Federation.
|
|
8
|
+
* 2. LES MEMES VERSIONS QUE SON JUMEAU (`starterWeb.jumeau`, le dossier du remote du meme service) :
|
|
9
|
+
* Angular et le socle frontal, pour que deux applications d'un service ne divergent pas en silence.
|
|
10
|
+
* 3. LE POIDS DE SON ECRAN (`starterWeb.ecran` : `{ "entree": "src/app/pages/connexion.ts",
|
|
11
|
+
* "seuilKo": 155 }`) : le chargement initial et la route de l'ecran, tout ce qu'ils importent
|
|
12
|
+
* statiquement, en brotli, lus dans la carte de la construction (`ng build --stats-json`, que
|
|
13
|
+
* `starter-web construire` pose pour ce role). Un seuil se baisse ; il ne se leve jamais sans
|
|
14
|
+
* mesure ecrite et datee (ADR 0031).
|
|
15
|
+
*
|
|
16
|
+
* Remplace `verification/application-autonome.mjs` que l'ecran anonyme d'identity tenait seul.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
import { cloture, departDeLaPage, totaux } from '../cloture.mjs';
|
|
21
|
+
import { fichiers, ko, lireJson, relatif } from '../commun.mjs';
|
|
22
|
+
|
|
23
|
+
/** Ce qui trahit un moteur de federation dans un fichier livre. */
|
|
24
|
+
const MOTEUR_DE_FEDERATION =
|
|
25
|
+
/es-module-shims|importShim|__NATIVE_FEDERATION__|@angular-architects\/native-federation/;
|
|
26
|
+
|
|
27
|
+
/** Les defauts de federation d'un dossier livre. */
|
|
28
|
+
export function federationLivree(sortie) {
|
|
29
|
+
const defauts = [];
|
|
30
|
+
for (const chemin of fichiers(sortie)) {
|
|
31
|
+
const nom = relatif(sortie, chemin);
|
|
32
|
+
if (/^remoteEntry/.test(nom)) {
|
|
33
|
+
defauts.push(`${nom} livre : une application autonome n'est pas un remote.`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (/\.(?:m?js|html)$/.test(nom) && MOTEUR_DE_FEDERATION.test(readFileSync(chemin, 'utf8'))) {
|
|
37
|
+
defauts.push(`${nom} : moteur de federation livre (es-module-shims ou Native Federation).`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const index = join(sortie, 'index.html');
|
|
41
|
+
if (existsSync(index) && /<script[^>]*type=["']?importmap/i.test(readFileSync(index, 'utf8'))) {
|
|
42
|
+
defauts.push("index.html : carte d'importation livree.");
|
|
43
|
+
}
|
|
44
|
+
return defauts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Les versions d'Angular et du socle qui different de celles du jumeau. */
|
|
48
|
+
export function versionsDuJumeau(paquet, jumeau) {
|
|
49
|
+
const siennes = { ...jumeau.dependencies, ...jumeau.devDependencies };
|
|
50
|
+
const defauts = [];
|
|
51
|
+
for (const [nom, version] of Object.entries({
|
|
52
|
+
...paquet.dependencies,
|
|
53
|
+
...paquet.devDependencies,
|
|
54
|
+
})) {
|
|
55
|
+
if (!nom.startsWith('@angular/') && !nom.startsWith('@starterlib/')) continue;
|
|
56
|
+
const duJumeau = siennes[nom];
|
|
57
|
+
if (duJumeau !== undefined && duJumeau !== version) {
|
|
58
|
+
defauts.push(
|
|
59
|
+
`${nom} ${String(version)} : le jumeau est en ${String(duJumeau)} (meme service, memes versions).`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return defauts;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Le morceau de l'entree `entree` dans la carte de la construction, ou `null`. */
|
|
67
|
+
export function morceauDeLEntree(carte, entree) {
|
|
68
|
+
for (const [sortie, description] of Object.entries(carte?.outputs ?? {})) {
|
|
69
|
+
if (description.entryPoint === entree && sortie.endsWith('.js')) {
|
|
70
|
+
return sortie.replace(/^.*browser\//, '');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Le poids de l'ecran : chargement initial, et initial plus la route de l'ecran. */
|
|
77
|
+
export function poidsDeLEcran(sortie, morceau) {
|
|
78
|
+
const depart = departDeLaPage(readFileSync(join(sortie, 'index.html'), 'utf8'));
|
|
79
|
+
const initial = totaux(cloture(sortie, depart, {}, 0));
|
|
80
|
+
const ecran = totaux(cloture(sortie, morceau === null ? depart : [...depart, morceau], {}, 0));
|
|
81
|
+
return { initial, ecran };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default {
|
|
85
|
+
nom: 'autonome',
|
|
86
|
+
roles: ['autonome'],
|
|
87
|
+
executer(projet) {
|
|
88
|
+
if (!existsSync(join(projet.sortie, 'index.html'))) {
|
|
89
|
+
return { defauts: [`${projet.sortieRelative}/index.html absent : construire d'abord.`] };
|
|
90
|
+
}
|
|
91
|
+
const reglages = projet.paquet.starterWeb ?? {};
|
|
92
|
+
const defauts = [...federationLivree(projet.sortie)];
|
|
93
|
+
const details = [];
|
|
94
|
+
|
|
95
|
+
if (typeof reglages.jumeau === 'string') {
|
|
96
|
+
const jumeau = lireJson(join(resolve(projet.racine, reglages.jumeau), 'package.json'));
|
|
97
|
+
if (jumeau === null)
|
|
98
|
+
defauts.push(`starterWeb.jumeau : ${reglages.jumeau}/package.json illisible.`);
|
|
99
|
+
else defauts.push(...versionsDuJumeau(projet.paquet, jumeau));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ecran = reglages.ecran;
|
|
103
|
+
if (ecran !== undefined) {
|
|
104
|
+
const carte = lireJson(join(dirname(projet.sortie), 'browser-stats.json'));
|
|
105
|
+
const morceau = carte === null ? null : morceauDeLEntree(carte, ecran.entree);
|
|
106
|
+
if (morceau === null) {
|
|
107
|
+
defauts.push(
|
|
108
|
+
`starterWeb.ecran : la route ${String(ecran.entree)} est introuvable dans browser-stats.json ` +
|
|
109
|
+
'(starter-web construire la pose : ng build --stats-json).',
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
const { initial, ecran: poids } = poidsDeLEcran(projet.sortie, morceau);
|
|
113
|
+
details.push(
|
|
114
|
+
`chargement initial ${String(ko(initial.brotli))} Ko br (${String(initial.fichiers)} fichiers)`,
|
|
115
|
+
`ecran ${String(ecran.entree)} ${String(ko(poids.brotli))} Ko br (${String(poids.fichiers)} fichiers, seuil ${String(ecran.seuilKo)} Ko br)`,
|
|
116
|
+
);
|
|
117
|
+
if (typeof ecran.seuilKo === 'number' && ko(poids.brotli) > ecran.seuilKo) {
|
|
118
|
+
defauts.push(
|
|
119
|
+
`ecran : ${String(ko(poids.brotli))} Ko br > ${String(ecran.seuilKo)} Ko br (un seuil ne se ` +
|
|
120
|
+
'leve jamais sans mesure ecrite et datee, ADR 0031).',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
defauts,
|
|
127
|
+
details,
|
|
128
|
+
resume: 'aucune federation livree, versions du jumeau et poids de l ecran conformes.',
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
package/src/etapes/budget.mjs
CHANGED
|
@@ -25,19 +25,23 @@ import { natureDesFichiers } from '../livraison.mjs';
|
|
|
25
25
|
export const BUDGETS_DU_REMOTE = { pageKo: 60, totalKo: 250, catalogueKo: 30 };
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Les seuils de l'hote, en Ko brotli (ADR 0031) :
|
|
29
|
-
* charge initiale, ramenes au socle 0.10.0 (releves
|
|
30
|
-
*
|
|
31
|
-
* 0.10.9, dont les controles d'empreinte S05 du lot 272 tournent au
|
|
32
|
-
* 282 et 301 (decision de l'orchestrateur sur autorisation du
|
|
33
|
-
* contre le socle 0.10.11 construit, avant publication, 281,0
|
|
34
|
-
* de page, banniere collee, marque textuelle, creneau de
|
|
28
|
+
* Les seuils de l'hote, en Ko brotli (ADR 0031) : 309 pour le premier ecran reel, 288 pour la
|
|
29
|
+
* charge initiale (ADR web 0005, D167). Leur histoire : ramenes au socle 0.10.0 (releves dans
|
|
30
|
+
* l'ADR 0031) ; la charge initiale passee de 280 a 281 le 2026-09-29, decision du proprietaire :
|
|
31
|
+
* 280,4 Ko mesures avec le socle 0.10.9, dont les controles d'empreinte S05 du lot 272 tournent au
|
|
32
|
+
* demarrage. Puis, le meme jour, 282 et 301 (decision de l'orchestrateur sur autorisation du
|
|
33
|
+
* proprietaire) : la coquille mesuree contre le socle 0.10.11 construit, avant publication, 281,0
|
|
34
|
+
* et 300,1 Ko (+0,4 Ko chacun : titre de page, banniere collee, marque textuelle, creneau de
|
|
35
|
+
* marque, demarrage determine, amorce). Puis 288 et 309 le 2026-10-07 : la coquille d'Aplomb
|
|
36
|
+
* mesuree contre la 0.11.0 construite, avant publication, 286,96 et 307,86 Ko (+5,1 et +6,2 Ko sur
|
|
37
|
+
* la 0.10.14 : barre inferieure, action critique et raccourci, ressource rafraichie, tronc), puis
|
|
38
|
+
* 287,4 et 308,3 Ko avec sa feuille de marque et la feuille de sa police de titre (+0,44 Ko).
|
|
35
39
|
*
|
|
36
40
|
* ⚠️ Ce ne sont pas la cible du plan SPA §2.8 (170 Ko). L'ecart restant est STRUCTUREL : un paquet
|
|
37
41
|
* partage en singleton part ENTIER, parce que n'importe quel remote peut en avoir besoin. Un seuil
|
|
38
42
|
* se baisse ; il ne se leve jamais sans mesure ecrite et sans date.
|
|
39
43
|
*/
|
|
40
|
-
export const BUDGETS_DE_L_HOTE = { premierEcranKo:
|
|
44
|
+
export const BUDGETS_DE_L_HOTE = { premierEcranKo: 309, chargeInitialeKo: 288, cibleDuPlanKo: 170 };
|
|
41
45
|
|
|
42
46
|
/** Les imports DYNAMIQUES d'un module : les pages qu'il charge a la demande. */
|
|
43
47
|
const IMPORT_DYNAMIQUE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
@@ -97,7 +97,7 @@ export function jugerLesDependances(paquet, outils = []) {
|
|
|
97
97
|
|
|
98
98
|
export default {
|
|
99
99
|
nom: 'dependances',
|
|
100
|
-
roles: ['remote', 'hote'],
|
|
100
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
101
101
|
executer(projet) {
|
|
102
102
|
const dossier = join(projet.racine, 'tools');
|
|
103
103
|
const outils = existsSync(dossier) ? readdirSync(dossier) : [];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les formulations qu'une application interdit dans ses textes affiches : une recherche textuelle
|
|
3
|
+
* simple dans ses catalogues, a chaque `verify` ; ni un controle du design, ni un jugement du sens
|
|
4
|
+
* (lot P09-27 d'Aplomb, generalise). L'etape `i18n` la lance.
|
|
5
|
+
*
|
|
6
|
+
* L'application declare sa liste : `package.json#starterWeb.formulationsInterdites`, chemin d'un
|
|
7
|
+
* fichier JSON relatif a sa racine. Le socle n'en connait aucune ; sans reglage, rien n'est cherche.
|
|
8
|
+
*
|
|
9
|
+
* {
|
|
10
|
+
* "formulations": [
|
|
11
|
+
* { "id": "F01", "textes": ["garanti", "garantie"], "langues": ["fr"] },
|
|
12
|
+
* { "id": "F07", "motif": "(?<![\\p{L}\\p{N}])[A-Z]-\\d{2}(?![\\p{L}\\p{N}])" }
|
|
13
|
+
* ],
|
|
14
|
+
* "exceptions": [
|
|
15
|
+
* { "formulation": "F01", "cle": "app.legal.absence", "raison": "...", "date": "2026-10-07" }
|
|
16
|
+
* ]
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* - `textes` : chaque forme se nomme (aucune flexion devinee) ; la recherche ignore la casse et les
|
|
20
|
+
* accents, confond les blancs (espaces insecables compris) et les tirets, et ne trouve un texte
|
|
21
|
+
* qu'en mots entiers : « garanti » ne se trouve pas dans « garantit ».
|
|
22
|
+
* - `motif` : une expression reguliere (drapeau `u`), appliquee au texte tel quel, casse comprise.
|
|
23
|
+
* - `langues` : parmi `fr` et `en` ; les deux par defaut.
|
|
24
|
+
* - une exception se nomme, se justifie et se date, pour une cle et une formulation ; celle dont la
|
|
25
|
+
* cle appartient a un perimetre du projet sans y exister est perimee : refusee.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Les blancs et les tirets, confondus en une espace. */
|
|
29
|
+
const BLANCS_ET_TIRETS = /[\s\p{Pd}]+/gu;
|
|
30
|
+
const BORD_AVANT = '(?<![\\p{L}\\p{N}])';
|
|
31
|
+
const BORD_APRES = '(?![\\p{L}\\p{N}])';
|
|
32
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
33
|
+
|
|
34
|
+
/** Le texte tel que la recherche le compare : sans casse ni accents, blancs et tirets confondus. */
|
|
35
|
+
export function normaliser(texte) {
|
|
36
|
+
return texte.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(BLANCS_ET_TIRETS, ' ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function echapper(texte) {
|
|
40
|
+
return texte.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function texteNonVide(valeur) {
|
|
44
|
+
return typeof valeur === 'string' && valeur.trim() !== '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Une formulation lue : `{ formulation }` prete a chercher, ou `{ defaut }`. */
|
|
48
|
+
function lireUneFormulation(entree, rang, langues, ids) {
|
|
49
|
+
const ou = `formulation ${String(rang + 1)}`;
|
|
50
|
+
if (entree === null || typeof entree !== 'object')
|
|
51
|
+
return { defaut: `${ou} : un objet est attendu.` };
|
|
52
|
+
const { id, textes, motif } = entree;
|
|
53
|
+
if (!texteNonVide(id)) return { defaut: `${ou} : "id" manquant.` };
|
|
54
|
+
if (ids.has(id)) return { defaut: `${id} : identifiant en double.` };
|
|
55
|
+
ids.add(id);
|
|
56
|
+
const cherchees = entree.langues ?? langues;
|
|
57
|
+
if (
|
|
58
|
+
!Array.isArray(cherchees) ||
|
|
59
|
+
cherchees.length === 0 ||
|
|
60
|
+
cherchees.some((langue) => !langues.includes(langue))
|
|
61
|
+
) {
|
|
62
|
+
return { defaut: `${id} : "langues" parmi ${langues.join(', ')}.` };
|
|
63
|
+
}
|
|
64
|
+
const motifs = [];
|
|
65
|
+
if (textes !== undefined) {
|
|
66
|
+
if (
|
|
67
|
+
!Array.isArray(textes) ||
|
|
68
|
+
textes.length === 0 ||
|
|
69
|
+
textes.some((texte) => typeof texte !== 'string' || normaliser(texte).trim() === '')
|
|
70
|
+
) {
|
|
71
|
+
return { defaut: `${id} : "textes", une liste de textes non vides, est attendu.` };
|
|
72
|
+
}
|
|
73
|
+
for (const texte of textes) {
|
|
74
|
+
const cherche = BORD_AVANT + echapper(normaliser(texte).trim()) + BORD_APRES;
|
|
75
|
+
motifs.push({ expression: new RegExp(cherche, 'u'), surLeTexteNormalise: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (motif !== undefined) {
|
|
79
|
+
if (!texteNonVide(motif))
|
|
80
|
+
return { defaut: `${id} : "motif", une expression non vide, est attendu.` };
|
|
81
|
+
try {
|
|
82
|
+
motifs.push({ expression: new RegExp(motif, 'u'), surLeTexteNormalise: false });
|
|
83
|
+
} catch (erreur) {
|
|
84
|
+
return {
|
|
85
|
+
defaut: `${id} : motif illisible (${erreur instanceof Error ? erreur.message : String(erreur)}).`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (motifs.length === 0) return { defaut: `${id} : "textes" ou "motif" attendu.` };
|
|
90
|
+
return { formulation: { id, langues: cherchees, motifs } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* La liste lue et jugee : les formulations pretes a chercher, les exceptions, et les defauts du
|
|
95
|
+
* fichier. Une liste fautive se refuse entiere : elle ne cherche rien de travers.
|
|
96
|
+
*/
|
|
97
|
+
export function lireLesFormulations(liste, langues) {
|
|
98
|
+
const defauts = [];
|
|
99
|
+
const formulations = [];
|
|
100
|
+
const exceptions = [];
|
|
101
|
+
if (liste === null || typeof liste !== 'object' || !Array.isArray(liste.formulations)) {
|
|
102
|
+
return {
|
|
103
|
+
formulations,
|
|
104
|
+
exceptions,
|
|
105
|
+
defauts: ['un objet { "formulations": [...] } est attendu.'],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const ids = new Set();
|
|
109
|
+
for (const [rang, entree] of liste.formulations.entries()) {
|
|
110
|
+
const lue = lireUneFormulation(entree, rang, langues, ids);
|
|
111
|
+
if (lue.defaut === undefined) formulations.push(lue.formulation);
|
|
112
|
+
else defauts.push(lue.defaut);
|
|
113
|
+
}
|
|
114
|
+
if (liste.exceptions !== undefined && !Array.isArray(liste.exceptions)) {
|
|
115
|
+
defauts.push('"exceptions" : une liste est attendue.');
|
|
116
|
+
}
|
|
117
|
+
for (const [rang, exception] of (Array.isArray(liste.exceptions)
|
|
118
|
+
? liste.exceptions
|
|
119
|
+
: []
|
|
120
|
+
).entries()) {
|
|
121
|
+
const ou = `exception ${String(rang + 1)}`;
|
|
122
|
+
if (exception === null || typeof exception !== 'object') {
|
|
123
|
+
defauts.push(`${ou} : un objet est attendu.`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const { formulation, cle, raison, date } = exception;
|
|
127
|
+
if (!ids.has(formulation))
|
|
128
|
+
defauts.push(`${ou} : formulation "${String(formulation)}" inconnue.`);
|
|
129
|
+
else if (!texteNonVide(cle)) defauts.push(`${ou} : "cle" manquante.`);
|
|
130
|
+
else if (!texteNonVide(raison))
|
|
131
|
+
defauts.push(`${ou} (${cle}) : "raison" manquante : une exception se justifie.`);
|
|
132
|
+
else if (typeof date !== 'string' || !DATE.test(date)) {
|
|
133
|
+
defauts.push(`${ou} (${cle}) : "date" AAAA-MM-JJ manquante : une exception se date.`);
|
|
134
|
+
} else exceptions.push({ formulation, cle });
|
|
135
|
+
}
|
|
136
|
+
return { formulations, exceptions, defauts };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Les textes des catalogues ou une formulation interdite se trouve, hors exception ; puis les
|
|
141
|
+
* exceptions perimees. `catalogues` : `{ <perimetre>: { fr: {...}, en: {...} } }`.
|
|
142
|
+
*/
|
|
143
|
+
export function jugerLesFormulations({ formulations, exceptions }, catalogues) {
|
|
144
|
+
const defauts = [];
|
|
145
|
+
const admises = new Set(exceptions.map(({ formulation, cle }) => `${formulation} ${cle}`));
|
|
146
|
+
for (const [perimetre, parLangue] of Object.entries(catalogues)) {
|
|
147
|
+
for (const [langue, catalogue] of Object.entries(parLangue)) {
|
|
148
|
+
if (catalogue === null || typeof catalogue !== 'object') continue;
|
|
149
|
+
for (const [cle, message] of Object.entries(catalogue)) {
|
|
150
|
+
if (typeof message !== 'string') continue;
|
|
151
|
+
const normalise = normaliser(message);
|
|
152
|
+
for (const { id, langues, motifs } of formulations) {
|
|
153
|
+
if (!langues.includes(langue) || admises.has(`${id} ${cle}`)) continue;
|
|
154
|
+
for (const { expression, surLeTexteNormalise } of motifs) {
|
|
155
|
+
const trouve = (surLeTexteNormalise ? normalise : message).match(expression);
|
|
156
|
+
if (trouve !== null) {
|
|
157
|
+
defauts.push(
|
|
158
|
+
`${perimetre}.${langue}.json : cle "${cle}", formulation interdite ${id} ("${trouve[0]}").`,
|
|
159
|
+
);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const { formulation, cle } of exceptions) {
|
|
168
|
+
const perimetre = cle.split('.')[0];
|
|
169
|
+
const parLangue = catalogues[perimetre];
|
|
170
|
+
if (parLangue === undefined) continue;
|
|
171
|
+
const presente = Object.values(parLangue).some(
|
|
172
|
+
(catalogue) =>
|
|
173
|
+
catalogue !== null && typeof catalogue === 'object' && Object.hasOwn(catalogue, cle),
|
|
174
|
+
);
|
|
175
|
+
if (!presente) {
|
|
176
|
+
defauts.push(
|
|
177
|
+
`exception ${formulation} : cle "${cle}" absente du perimetre "${perimetre}" (exception perimee, a retirer).`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return defauts;
|
|
182
|
+
}
|
package/src/etapes/i18n.mjs
CHANGED
|
@@ -18,13 +18,16 @@
|
|
|
18
18
|
* - l'hote porte les cles que `@starterlib/core` traduit par son catalogue
|
|
19
19
|
* (`schemas/cles-exigees-du-socle.json`, constat A0837) ;
|
|
20
20
|
* - le type des cles engendre (`src/app/i18n/cles.ts`, `starter-web i18n:types`) est a jour : une cle
|
|
21
|
-
* inconnue dans `t()` est alors une erreur de compilation
|
|
21
|
+
* inconnue dans `t()` est alors une erreur de compilation ;
|
|
22
|
+
* - aucun texte ne porte une formulation que l'application interdit
|
|
23
|
+
* (`starterWeb.formulationsInterdites`, voir `formulations.mjs`), hors exception nommee et datee.
|
|
22
24
|
*/
|
|
23
25
|
import { existsSync, readFileSync } from 'node:fs';
|
|
24
|
-
import { join } from 'node:path';
|
|
26
|
+
import { join, resolve } from 'node:path';
|
|
25
27
|
import { fichiers, lireJson, relatif } from '../commun.mjs';
|
|
26
28
|
import { dossierInstalle } from '../socle-installe.mjs';
|
|
27
29
|
import { cheminDesTypesDeCles, typesDeCles } from '../construction/types-i18n.mjs';
|
|
30
|
+
import { jugerLesFormulations, lireLesFormulations } from './formulations.mjs';
|
|
28
31
|
|
|
29
32
|
export const LANGUES = ['fr', 'en'];
|
|
30
33
|
|
|
@@ -264,12 +267,50 @@ function lire(chemin) {
|
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
|
|
270
|
+
/**
|
|
271
|
+
* La recherche des formulations interdites, si l'application en declare : `cherchees` vaut `null`
|
|
272
|
+
* sans reglage, le nombre de formulations cherchees sinon.
|
|
273
|
+
*/
|
|
274
|
+
export function rechercherLesFormulations(projet, catalogues) {
|
|
275
|
+
const reglage = projet.paquet?.starterWeb?.formulationsInterdites;
|
|
276
|
+
if (reglage === undefined) return { defauts: [], cherchees: null };
|
|
277
|
+
if (typeof reglage !== 'string' || reglage.trim() === '') {
|
|
278
|
+
return {
|
|
279
|
+
defauts: [
|
|
280
|
+
"package.json : starterWeb.formulationsInterdites, le chemin d'un fichier JSON, est attendu.",
|
|
281
|
+
],
|
|
282
|
+
cherchees: 0,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const liste = lire(resolve(projet.racine, reglage));
|
|
286
|
+
if (liste === null) {
|
|
287
|
+
return {
|
|
288
|
+
defauts: [`${reglage} : absent ou illisible (starterWeb.formulationsInterdites).`],
|
|
289
|
+
cherchees: 0,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const lue = lireLesFormulations(liste, LANGUES);
|
|
293
|
+
if (lue.defauts.length > 0) {
|
|
294
|
+
return { defauts: lue.defauts.map((defaut) => `${reglage} : ${defaut}`), cherchees: 0 };
|
|
295
|
+
}
|
|
296
|
+
return { defauts: jugerLesFormulations(lue, catalogues), cherchees: lue.formulations.length };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Les perimetres de catalogues d'un projet : ceux du manifeste (remote), ceux de
|
|
301
|
+
* `starterWeb.i18n.perimetres` (application autonome, lot 240), `socle` pour l'hote.
|
|
302
|
+
*/
|
|
303
|
+
export function perimetresDuProjet(projet) {
|
|
304
|
+
if (projet.role === 'remote') return projet.source?.i18n?.perimetres ?? [];
|
|
305
|
+
if (projet.role === 'autonome') return projet.paquet.starterWeb?.i18n?.perimetres ?? ['socle'];
|
|
306
|
+
return ['socle'];
|
|
307
|
+
}
|
|
308
|
+
|
|
267
309
|
export default {
|
|
268
310
|
nom: 'i18n',
|
|
269
|
-
roles: ['remote', 'hote'],
|
|
311
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
270
312
|
executer(projet) {
|
|
271
|
-
const perimetres =
|
|
272
|
-
projet.role === 'remote' ? (projet.source?.i18n?.perimetres ?? []) : ['socle'];
|
|
313
|
+
const perimetres = perimetresDuProjet(projet);
|
|
273
314
|
if (perimetres.length === 0) {
|
|
274
315
|
return { defauts: ['manifest.source.json : i18n.perimetres vide, aucun catalogue a lire.'] };
|
|
275
316
|
}
|
|
@@ -305,8 +346,9 @@ export default {
|
|
|
305
346
|
// L'aide contextuelle de chaque page du manifeste, et aucune aide sans page (lot 280).
|
|
306
347
|
defauts.push(...jugerLesAides(catalogues, projet.source?.routes ?? []));
|
|
307
348
|
|
|
308
|
-
// L'hote
|
|
309
|
-
|
|
349
|
+
// L'hote, et l'application autonome qui pose le noyau comme lui, portent les cles que le noyau
|
|
350
|
+
// traduit par son catalogue.
|
|
351
|
+
if (projet.role === 'hote' || projet.role === 'autonome') {
|
|
310
352
|
const dossier = dossierInstalle(projet.racine, '@starterlib/core');
|
|
311
353
|
const exigees =
|
|
312
354
|
dossier === null ? null : lireJson(join(dossier, 'schemas', 'cles-exigees-du-socle.json'));
|
|
@@ -321,6 +363,10 @@ export default {
|
|
|
321
363
|
}
|
|
322
364
|
}
|
|
323
365
|
|
|
366
|
+
// Les formulations que l'application interdit dans ses textes (P09-27 d'Aplomb, generalise).
|
|
367
|
+
const recherche = rechercherLesFormulations(projet, catalogues);
|
|
368
|
+
defauts.push(...recherche.defauts);
|
|
369
|
+
|
|
324
370
|
// Le type des cles engendre, a jour.
|
|
325
371
|
const cheminDesTypes = cheminDesTypesDeCles(projet.racine);
|
|
326
372
|
const attendu = typesDeCles(perimetres, catalogues);
|
|
@@ -332,7 +378,11 @@ export default {
|
|
|
332
378
|
|
|
333
379
|
return {
|
|
334
380
|
defauts,
|
|
335
|
-
resume:
|
|
381
|
+
resume:
|
|
382
|
+
`${String(perimetres.length)} perimetre(s), ${String(cles)} cle(s) en fr et en en, messages lisibles, cles appelees presentes` +
|
|
383
|
+
(recherche.cherchees === null
|
|
384
|
+
? '.'
|
|
385
|
+
: `, ${String(recherche.cherchees)} formulation(s) interdite(s) cherchee(s).`),
|
|
336
386
|
};
|
|
337
387
|
},
|
|
338
388
|
};
|
package/src/etapes/jetons.mjs
CHANGED
|
@@ -11,15 +11,24 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Sont lues les feuilles `.scss` et `.css` sous `src/`, et les styles EN LIGNE des composants
|
|
13
13
|
* (`styles: \`...\``), que la copie de la coquille ne voyait pas.
|
|
14
|
+
*
|
|
15
|
+
* La MARQUE de la SPA (niveau 2 bis : `marque.json`, `marque.ts` ou `marque.mjs` a la racine) est
|
|
16
|
+
* rejouee ici a chaque verification, par le moteur des jetons installe : une valeur reprise qui fait
|
|
17
|
+
* tomber une paire sous son seuil rougit `verify`, paire nommee, avant la construction.
|
|
14
18
|
*/
|
|
15
19
|
import { readFileSync } from 'node:fs';
|
|
16
20
|
import { join } from 'node:path';
|
|
17
21
|
import { fichiers, relatif } from '../commun.mjs';
|
|
22
|
+
import { fichierDeMarque, jugerLaMarque } from '../marque.mjs';
|
|
18
23
|
|
|
19
24
|
/** Poses a l'execution par un composant du socle, donc absents des feuilles engendrees. */
|
|
20
25
|
const POSES_A_L_EXECUTION = new Set([
|
|
21
26
|
// st-app-shell mesure l'en-tete collant et publie sa hauteur (scroll-padding de la page).
|
|
22
27
|
'--st-hauteur-entete',
|
|
28
|
+
// st-bottom-nav (P08-05) et l'action permanente flottante de st-app-shell (P08-06) publient la
|
|
29
|
+
// hauteur qu'elles couvrent au bas de la fenetre.
|
|
30
|
+
'--st-hauteur-pied',
|
|
31
|
+
'--st-hauteur-action',
|
|
23
32
|
]);
|
|
24
33
|
|
|
25
34
|
/** Une lecture de jeton : son nom, et le caractere qui suit (`,` : avec repli ; `)` : sans). */
|
|
@@ -67,8 +76,8 @@ export function jugerLesJetons(feuilles, definis) {
|
|
|
67
76
|
|
|
68
77
|
export default {
|
|
69
78
|
nom: 'jetons',
|
|
70
|
-
roles: ['remote', 'hote'],
|
|
71
|
-
executer(projet) {
|
|
79
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
80
|
+
async executer(projet) {
|
|
72
81
|
const socle = join(projet.racine, 'node_modules', '@starterlib', 'tokens', 'styles');
|
|
73
82
|
const definis = new Set();
|
|
74
83
|
for (const feuille of fichiers(socle).filter((chemin) => chemin.endsWith('.css'))) {
|
|
@@ -99,9 +108,19 @@ export default {
|
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
const { defauts, lues } = jugerLesJetons(feuilles, definis);
|
|
111
|
+
let marque = '';
|
|
112
|
+
const chemin = fichierDeMarque(projet.racine);
|
|
113
|
+
if (chemin !== null) {
|
|
114
|
+
const nom = relatif(projet.racine, chemin);
|
|
115
|
+
const juge = await jugerLaMarque(projet.racine, chemin);
|
|
116
|
+
for (const defaut of juge.defauts) defauts.push(`${nom} : ${defaut}`);
|
|
117
|
+
if (juge.defauts.length === 0) marque = ` ; marque ${String(juge.valeurs.nom)} conforme`;
|
|
118
|
+
}
|
|
102
119
|
return {
|
|
103
120
|
defauts,
|
|
104
|
-
resume:
|
|
121
|
+
resume:
|
|
122
|
+
`${String(lues)} lecture(s) sans repli dans ${String(feuilles.length)} feuille(s), toutes ` +
|
|
123
|
+
`definies${marque}.`,
|
|
105
124
|
};
|
|
106
125
|
},
|
|
107
126
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Etape `navigateurs` (lot 276) : la liste browserslist de la SPA est celle du socle, lue dans la
|
|
3
|
+
* matrice publiee avec ce CLI (`src/navigateurs.mjs`). Ni plus bas, ni plus haut, ni absente.
|
|
4
|
+
*/
|
|
5
|
+
import { jugerLesNavigateurs, navigateursDeLaSpa, navigateursDuSocle } from '../navigateurs.mjs';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
nom: 'navigateurs',
|
|
9
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
10
|
+
executer(projet) {
|
|
11
|
+
const attendus = navigateursDuSocle();
|
|
12
|
+
const lus = navigateursDeLaSpa(projet.racine);
|
|
13
|
+
const defauts = jugerLesNavigateurs(lus, attendus);
|
|
14
|
+
return defauts.length > 0
|
|
15
|
+
? { defauts }
|
|
16
|
+
: {
|
|
17
|
+
defauts,
|
|
18
|
+
resume: `${lus?.origine ?? ''} conforme au plancher du socle (${String(attendus.length)} requetes).`,
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
};
|
package/src/etapes/outillage.mjs
CHANGED
|
@@ -34,6 +34,49 @@ export function jugerLesSelecteurs(prefixe, sources) {
|
|
|
34
34
|
return defauts;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Les regles de style du socle (P08-11 : seul le socle fait le design) : stylelint pour les feuilles,
|
|
39
|
+
* le greffon pour les gabarits. Les lever exige une exemption NOMMEE et DATEE.
|
|
40
|
+
*/
|
|
41
|
+
const REGLES_DE_STYLE =
|
|
42
|
+
/\b(?:color-no-hex|color-named|declaration-property-value-(?:dis)?allowed-list|property-disallowed-list|socle\/no-style-literal|socle\/variantes-reservees)\b/;
|
|
43
|
+
|
|
44
|
+
/** Un commentaire qui leve des regles : `stylelint-disable-next-line <regles> -- <raison>`. */
|
|
45
|
+
const EXEMPTION = /(stylelint|eslint)-disable(?:-next-line|-line)?\b(.*?)(?:\*\/|-->|$)/gm;
|
|
46
|
+
|
|
47
|
+
/** Une raison qui commence par sa date : `-- 2026-10-07 : <raison>`. */
|
|
48
|
+
const DATEE = /--\s*\d{4}-\d{2}-\d{2}\s*:?\s*\S/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Les exemptions des regles de style (P08-11) dans les sources d'une SPA (`{ nom, texte }` : feuilles,
|
|
52
|
+
* gabarits, code) : chacune nommee ET datee (`-- AAAA-MM-JJ : <raison>`), pour se journaliser dans le
|
|
53
|
+
* CHANGELOG de l'application. Une levee de TOUTES les regles (sans liste) dans une feuille ou un
|
|
54
|
+
* gabarit compte comme une exemption de style. Rend `{ defauts, datees }`.
|
|
55
|
+
*/
|
|
56
|
+
export function jugerLesExemptions(sources) {
|
|
57
|
+
const defauts = [];
|
|
58
|
+
let datees = 0;
|
|
59
|
+
for (const { nom, texte } of sources) {
|
|
60
|
+
const codeSeul = nom.endsWith('.ts');
|
|
61
|
+
for (const exemption of texte.matchAll(EXEMPTION)) {
|
|
62
|
+
const suite = exemption[2] ?? '';
|
|
63
|
+
const regles = suite.split('--')[0] ?? '';
|
|
64
|
+
const toutes = regles.trim() === '';
|
|
65
|
+
if (toutes ? codeSeul : !REGLES_DE_STYLE.test(regles)) continue;
|
|
66
|
+
if (DATEE.test(suite)) {
|
|
67
|
+
datees += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const ligne = texte.slice(0, exemption.index).split('\n').length;
|
|
71
|
+
defauts.push(
|
|
72
|
+
`${nom}:${String(ligne)} : exemption de style sans date ni raison ; ecrire ` +
|
|
73
|
+
'`-- AAAA-MM-JJ : <raison>` et la journaliser dans le CHANGELOG de l application.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { defauts, datees };
|
|
78
|
+
}
|
|
79
|
+
|
|
37
80
|
/** Le resultat d'une commande deleguee : `resume` si elle reussit, son code sinon. */
|
|
38
81
|
function deleguer(projet, arguments_, resume) {
|
|
39
82
|
const code = executer('pnpm', arguments_, projet.racine);
|
|
@@ -44,7 +87,7 @@ function deleguer(projet, arguments_, resume) {
|
|
|
44
87
|
|
|
45
88
|
export const format = {
|
|
46
89
|
nom: 'format',
|
|
47
|
-
roles: ['remote', 'hote'],
|
|
90
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
48
91
|
executer: (projet) =>
|
|
49
92
|
deleguer(projet, ['exec', 'prettier', '--check', '.'], 'prettier --check . conforme.'),
|
|
50
93
|
};
|
|
@@ -59,7 +102,7 @@ const CONFIGURATIONS_STYLELINT = [
|
|
|
59
102
|
|
|
60
103
|
export const lint = {
|
|
61
104
|
nom: 'lint',
|
|
62
|
-
roles: ['remote', 'hote'],
|
|
105
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
63
106
|
preparation: 'sprite',
|
|
64
107
|
executer(projet) {
|
|
65
108
|
const sources = fichiers(join(projet.racine, 'src'))
|
|
@@ -70,6 +113,21 @@ export const lint = {
|
|
|
70
113
|
}));
|
|
71
114
|
const selecteurs = jugerLesSelecteurs(projet.prefixe, sources);
|
|
72
115
|
if (selecteurs.length > 0) return { defauts: selecteurs };
|
|
116
|
+
// P08-11 : une exemption de style est nommee et datee, jamais muette.
|
|
117
|
+
const exemptions = jugerLesExemptions(
|
|
118
|
+
fichiers(join(projet.racine, 'src'))
|
|
119
|
+
.filter((chemin) => /\.(scss|css|html|ts)$/.test(chemin))
|
|
120
|
+
.filter((chemin) => !chemin.includes(DOSSIER_DU_CLIENT_ENGENDRE))
|
|
121
|
+
.map((chemin) => ({
|
|
122
|
+
nom: relatif(projet.racine, chemin),
|
|
123
|
+
texte: readFileSync(chemin, 'utf8'),
|
|
124
|
+
})),
|
|
125
|
+
);
|
|
126
|
+
if (exemptions.defauts.length > 0) return { defauts: exemptions.defauts };
|
|
127
|
+
const ecartes =
|
|
128
|
+
exemptions.datees === 0
|
|
129
|
+
? ''
|
|
130
|
+
: ` ; ${String(exemptions.datees)} exemption(s) de style datee(s), a journaliser dans le CHANGELOG`;
|
|
73
131
|
// Les feuilles sous stylelint (lot 178) : jetons, pixels, plans, ordre du DOM. La configuration
|
|
74
132
|
// etend celle du socle (@starterlib/eslint-config/stylelint) ; sans elle, rien n'est garde.
|
|
75
133
|
if (!CONFIGURATIONS_STYLELINT.some((nom) => existsSync(join(projet.racine, nom)))) {
|
|
@@ -88,14 +146,14 @@ export const lint = {
|
|
|
88
146
|
return deleguer(
|
|
89
147
|
projet,
|
|
90
148
|
['exec', 'stylelint', 'src/**/*.{scss,css}', '--allow-empty-input'],
|
|
91
|
-
`selecteurs en ${String(projet.prefixe)}-, eslint . et stylelint conformes.`,
|
|
149
|
+
`selecteurs en ${String(projet.prefixe)}-, eslint . et stylelint conformes${ecartes}.`,
|
|
92
150
|
);
|
|
93
151
|
},
|
|
94
152
|
};
|
|
95
153
|
|
|
96
154
|
export const test = {
|
|
97
155
|
nom: 'test',
|
|
98
|
-
roles: ['remote', 'hote'],
|
|
156
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
99
157
|
preparation: 'sprite',
|
|
100
158
|
executer: (projet) => deleguer(projet, ['exec', 'ng', 'test', '--no-watch'], 'ng test conforme.'),
|
|
101
159
|
};
|
|
@@ -136,7 +194,7 @@ export function motifsNonPortables(racine) {
|
|
|
136
194
|
*/
|
|
137
195
|
export const contrat = {
|
|
138
196
|
nom: 'contrat',
|
|
139
|
-
roles: ['remote', 'hote'],
|
|
197
|
+
roles: ['remote', 'hote', 'autonome'],
|
|
140
198
|
executer(projet) {
|
|
141
199
|
if (projet.paquet.scripts?.['contrat:check'] === undefined) {
|
|
142
200
|
return { sautee: 'aucun script contrat:check : pas de client engendre (ADR web 0008).' };
|