@starterlib/cli 0.10.2 → 0.10.4
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/matrice-compatibilite.md +3 -2
- package/package.json +1 -1
- package/src/construction/manifeste.mjs +14 -6
- package/src/construction/sprite.mjs +23 -0
- package/src/construction/types-i18n.mjs +11 -5
- package/src/etapes/outillage.mjs +39 -4
- package/src/etapes/securite.mjs +27 -6
- package/src/verifier.mjs +6 -4
package/matrice-compatibilite.md
CHANGED
|
@@ -90,5 +90,6 @@ partout d'un coup.
|
|
|
90
90
|
`transloco` entrera ici quand le premier depot de SPA l'emploiera ; il declare
|
|
91
91
|
une plage de pairs tres large (`>=16`), qui ne vaut pas compatibilite testee
|
|
92
92
|
avec la 22. `orval` y est depuis l'ADR web 0008, a la version relevee le
|
|
93
|
-
2026-09-25 :
|
|
94
|
-
|
|
93
|
+
2026-09-25 : sa compatibilite avec la 22 est prouvee depuis le lot 175 (2026-09-28), par les
|
|
94
|
+
clients engendres de la coquille, du temoin, de catalog et d'identity, construits et essayes
|
|
95
|
+
contre Angular 22.2.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starterlib/cli",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
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",
|
|
@@ -128,13 +128,18 @@ export function ecrireLeManifeste(projet) {
|
|
|
128
128
|
`${prefixe}/i18n/${hacherLeCatalogue(join(projet.sortie, 'i18n'), nom, langue)}`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
131
|
+
// Le sprite annonce est celui de la SOURCE, nomme par son empreinte (lot 194) : jamais le premier
|
|
132
|
+
// fichier venu d'un dossier ou deux sprites haches pouvaient coexister (le temoin y avait porte
|
|
133
|
+
// son sprite metier et une copie de celui du socle, et le manifeste pouvait annoncer l'autre).
|
|
134
|
+
const spriteSource = join(projet.racine, 'src', 'icones', 'sprite.svg');
|
|
135
|
+
if (!existsSync(spriteSource)) {
|
|
136
|
+
throw new Error('sprite metier absent : src/icones/sprite.svg.');
|
|
137
|
+
}
|
|
138
|
+
const sprite = `sprite-${empreinteCourte(readFileSync(spriteSource))}.svg`;
|
|
139
|
+
if (!existsSync(join(projet.sortie, 'icones', sprite))) {
|
|
136
140
|
throw new Error(
|
|
137
|
-
|
|
141
|
+
`${projet.sortieRelative}/icones/${sprite} absent (le sprite de src/icones/sprite.svg) : ` +
|
|
142
|
+
"starter-web sprite, puis construire, d'abord.",
|
|
138
143
|
);
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -152,6 +157,9 @@ export function ecrireLeManifeste(projet) {
|
|
|
152
157
|
routes: source.routes,
|
|
153
158
|
i18n: { perimetres: source.i18n?.perimetres, catalogues },
|
|
154
159
|
sprite: `${prefixe}/icones/${sprite}`,
|
|
160
|
+
// Les politiques Trusted Types que le remote cree lui-meme (lot 270), comparees par le BFF a sa
|
|
161
|
+
// CSP : recopiees telles que declarees, absentes si le remote n'en cree aucune.
|
|
162
|
+
...(source.trustedTypes === undefined ? {} : { trustedTypes: source.trustedTypes }),
|
|
155
163
|
apiExigee: source.apiExigee,
|
|
156
164
|
};
|
|
157
165
|
writeFileSync(join(projet.sortie, 'manifest.json'), `${JSON.stringify(manifeste, null, 2)}\n`);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Une ancienne empreinte est retiree : deux sprites livres, et le manifeste pourrait annoncer le
|
|
14
14
|
* mauvais.
|
|
15
15
|
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import { join } from 'node:path';
|
|
18
19
|
import { empreinteCourte } from '../commun.mjs';
|
|
@@ -47,6 +48,27 @@ function reglages(projet) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* L'hote livre le sprite du socle tel que `@starterlib/ui` l'a publie (lot 194) : le
|
|
53
|
+
* `catalogue.json` publie a cote en porte l'empreinte SHA-256. Un sprite recopie a la main, tronque
|
|
54
|
+
* ou d'une autre version que son catalogue arrete la construction, au lieu de livrer des icones que
|
|
55
|
+
* le catalogue ne decrit pas.
|
|
56
|
+
*/
|
|
57
|
+
function verifierLeSpriteDuSocle(source, contenu) {
|
|
58
|
+
const chemin = join(source, '..', 'catalogue.json');
|
|
59
|
+
if (!existsSync(chemin)) {
|
|
60
|
+
throw new Error(`catalogue du sprite du socle absent : ${chemin}.`);
|
|
61
|
+
}
|
|
62
|
+
const annonce = JSON.parse(readFileSync(chemin, 'utf8')).sprite ?? {};
|
|
63
|
+
const lue = createHash('sha256').update(contenu).digest('hex');
|
|
64
|
+
if (annonce.sha256 !== lue) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`sprite du socle altere : empreinte ${empreinteCourte(contenu)}, le catalogue de ` +
|
|
67
|
+
`@starterlib/ui annonce ${String(annonce.empreinte)} (${chemin}).`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
/** Pose le sprite hache et sa constante ; rend l'adresse servie. */
|
|
51
73
|
export function poserLeSprite(projet) {
|
|
52
74
|
const { source, dossier, adresse, genere, constante } = reglages(projet);
|
|
@@ -56,6 +78,7 @@ export function poserLeSprite(projet) {
|
|
|
56
78
|
);
|
|
57
79
|
}
|
|
58
80
|
const contenu = readFileSync(source);
|
|
81
|
+
if (projet.role === 'hote') verifierLeSpriteDuSocle(source, contenu);
|
|
59
82
|
const fichier = `sprite-${empreinteCourte(contenu)}.svg`;
|
|
60
83
|
mkdirSync(dossier, { recursive: true });
|
|
61
84
|
for (const existant of readdirSync(dossier)) {
|
|
@@ -34,16 +34,22 @@ export function typesDeCles(perimetres, catalogues) {
|
|
|
34
34
|
for (const perimetre of perimetres) {
|
|
35
35
|
const cles = Object.keys(catalogues[perimetre]?.fr ?? {}).sort();
|
|
36
36
|
lignes.push('', `/** Les cles du perimetre ${perimetre}. */`);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
37
|
+
const union = cles.length === 0 ? 'never' : cles.map((cle) => `'${cle}'`).join(' | ');
|
|
38
|
+
const enLigne = `export type ${nomDuType(perimetre)} = ${union};`;
|
|
39
|
+
// La forme que Prettier (largeur 100) donne, dans ses trois cas : une ligne si elle tient ;
|
|
40
|
+
// sinon l'union sur sa propre ligne indentee, si elle y tient (0.10.3 : la 0.10.2 sautait ce
|
|
41
|
+
// cas, et un petit catalogue, le squelette de new-spa.sh pour un nom de cinq lettres, sortait
|
|
42
|
+
// en echec de `prettier --check`) ; sinon une cle par ligne. Le fichier engendre passe ainsi
|
|
43
|
+
// `prettier --check` sans etre ecarte (test/i18n.test.mjs le compare a Prettier).
|
|
42
44
|
if (enLigne.length <= 100) {
|
|
43
45
|
lignes.push(enLigne);
|
|
44
46
|
continue;
|
|
45
47
|
}
|
|
46
48
|
lignes.push(`export type ${nomDuType(perimetre)} =`);
|
|
49
|
+
if (` ${union};`.length <= 100 || cles.length === 1) {
|
|
50
|
+
lignes.push(` ${union};`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
47
53
|
cles.forEach((cle, indice) => {
|
|
48
54
|
lignes.push(` | '${cle}'${indice === cles.length - 1 ? ';' : ''}`);
|
|
49
55
|
});
|
package/src/etapes/outillage.mjs
CHANGED
|
@@ -100,17 +100,52 @@ export const test = {
|
|
|
100
100
|
executer: (projet) => deleguer(projet, ['exec', 'ng', 'test', '--no-watch'], 'ng test conforme.'),
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
/** Ou vit le client engendre (ADR web 0008). */
|
|
104
|
+
export const DOSSIER_DU_CLIENT_ENGENDRE = join('src', 'app', 'donnees', 'genere');
|
|
105
|
+
|
|
106
|
+
/** Un motif du contrat, tel qu'orval le recopie en commentaire du client engendre. */
|
|
107
|
+
const MOTIF_ENGENDRE = /@pattern\s+(\S.*?)\s*$/gm;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Les motifs du client engendre qui ne compilent pas en ECMA-262 (lot 175, report du lot 81) : un
|
|
111
|
+
* motif Java (`\p{Cntrl}`, quantificateur possessif, groupe atomique) recopie par le contrat. Il
|
|
112
|
+
* refuserait chaque saisie legitime, ou ne compilerait pas, le jour ou un formulaire ou un schema
|
|
113
|
+
* de validation le reprendrait. Lu avec le drapeau `u`, celui d'un motif HTML.
|
|
114
|
+
*/
|
|
115
|
+
export function motifsNonPortables(racine) {
|
|
116
|
+
const defauts = [];
|
|
117
|
+
for (const chemin of fichiers(join(racine, DOSSIER_DU_CLIENT_ENGENDRE))) {
|
|
118
|
+
if (!chemin.endsWith('.ts')) continue;
|
|
119
|
+
for (const [, motif] of readFileSync(chemin, 'utf8').matchAll(MOTIF_ENGENDRE)) {
|
|
120
|
+
try {
|
|
121
|
+
new RegExp(motif, 'u');
|
|
122
|
+
} catch (erreur) {
|
|
123
|
+
defauts.push(
|
|
124
|
+
`${relatif(racine, chemin)} : motif ${motif} hors ECMA-262 (${erreur instanceof Error ? erreur.message : String(erreur)}) ; le corriger dans le contrat du service.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return defauts;
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
/**
|
|
104
|
-
* `contrat` : le client d'API engendre depuis le contrat du service, sans difference (lot 175)
|
|
105
|
-
* Tant que la SPA n'a pas de script `contrat:check`,
|
|
133
|
+
* `contrat` : le client d'API engendre depuis le contrat du service, sans difference (lot 175), et
|
|
134
|
+
* sans motif qu'un navigateur ne sait pas lire. Tant que la SPA n'a pas de script `contrat:check`,
|
|
135
|
+
* l'etape est sautee, et le dit.
|
|
106
136
|
*/
|
|
107
137
|
export const contrat = {
|
|
108
138
|
nom: 'contrat',
|
|
109
139
|
roles: ['remote', 'hote'],
|
|
110
140
|
executer(projet) {
|
|
111
141
|
if (projet.paquet.scripts?.['contrat:check'] === undefined) {
|
|
112
|
-
return { sautee: 'aucun script contrat:check
|
|
142
|
+
return { sautee: 'aucun script contrat:check : pas de client engendre (ADR web 0008).' };
|
|
113
143
|
}
|
|
114
|
-
|
|
144
|
+
const resultat = deleguer(projet, ['run', 'contrat:check'], 'client engendre sans difference.');
|
|
145
|
+
if (resultat.defauts.length > 0) return resultat;
|
|
146
|
+
const motifs = motifsNonPortables(projet.racine);
|
|
147
|
+
return motifs.length === 0
|
|
148
|
+
? { defauts: [], resume: 'client engendre sans difference, motifs ECMA-262.' }
|
|
149
|
+
: { defauts: motifs };
|
|
115
150
|
},
|
|
116
151
|
};
|
package/src/etapes/securite.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Pour tout fichier livre, cadre compris :
|
|
7
7
|
* - eval, new Function, stockage du navigateur, lecture de cookie ;
|
|
8
8
|
* - politiques Trusted Types comparees a la liste EXACTE du BFF
|
|
9
|
-
* (`platform.web.headers.csp.trusted-types-policies`)
|
|
9
|
+
* (`platform.web.headers.csp.trusted-types-policies`), augmentee de celles que le remote
|
|
10
|
+
* declare (`trustedTypes` de `manifest.source.json`, lot 270) ;
|
|
10
11
|
* - cartes de source (et leurs variantes compressees), script dans un SVG ;
|
|
11
12
|
* - dans une page HTML : balise meta de politique de securite, script ou feuille EN LIGNE.
|
|
12
13
|
*
|
|
@@ -28,7 +29,7 @@ import { join } from 'node:path';
|
|
|
28
29
|
import { fichiers, lireJson, relatif } from '../commun.mjs';
|
|
29
30
|
import { natureDesFichiers } from '../livraison.mjs';
|
|
30
31
|
|
|
31
|
-
/** Les politiques que la CSP du BFF laisse creer (application.yml du BFF). */
|
|
32
|
+
/** Les politiques que la CSP du BFF laisse creer par defaut (application.yml du BFF). */
|
|
32
33
|
export const POLITIQUES_ADMISES = ['angular', 'angular#components', 'es-module-shims', 'nfo'];
|
|
33
34
|
|
|
34
35
|
const CREATION_DE_POLITIQUE = /trustedTypes\s*\.\s*createPolicy\(\s*['"]([^'"]*)['"]/g;
|
|
@@ -165,7 +166,11 @@ function jugerUnePage(nom, contenu) {
|
|
|
165
166
|
* Les defauts d'un fichier livre `nom` (relatif au dossier livre) de contenu `contenu`. `role` :
|
|
166
167
|
* `remote` ou `hote` ; `partage` : le fichier est du code du cadre, partage par la federation.
|
|
167
168
|
*/
|
|
168
|
-
export function jugerUnFichierLivre(
|
|
169
|
+
export function jugerUnFichierLivre(
|
|
170
|
+
nom,
|
|
171
|
+
contenu,
|
|
172
|
+
{ role = 'remote', partage = false, declarees = [] } = {},
|
|
173
|
+
) {
|
|
169
174
|
// Une carte, ou sa variante compressee : `/x.js.map.br` se servirait aussi bien que l'original.
|
|
170
175
|
if (/\.map(?:\.br|\.gz)?$/.test(nom)) {
|
|
171
176
|
return [`${nom} : carte de source livree (sourceMap doit rester false en production).`];
|
|
@@ -190,10 +195,11 @@ export function jugerUnFichierLivre(nom, contenu, { role = 'remote', partage = f
|
|
|
190
195
|
const duCadre = POLITIQUES_DU_CADRE.some(
|
|
191
196
|
(exception) => exception.fichier.test(nom) && exception.politique === politique,
|
|
192
197
|
);
|
|
193
|
-
if (!POLITIQUES_ADMISES.includes(politique) && !duCadre) {
|
|
198
|
+
if (!POLITIQUES_ADMISES.includes(politique) && !declarees.includes(politique) && !duCadre) {
|
|
194
199
|
defauts.push(
|
|
195
200
|
`${nom} : politique Trusted Types hors liste ("${politique}" ; admises : ` +
|
|
196
|
-
`${POLITIQUES_ADMISES.join(', ')}). La declarer
|
|
201
|
+
`${[...POLITIQUES_ADMISES, ...declarees].join(', ')}). La declarer dans ` +
|
|
202
|
+
'manifest.source.json (trustedTypes) et a la CSP du BFF avant livraison.',
|
|
197
203
|
);
|
|
198
204
|
}
|
|
199
205
|
}
|
|
@@ -223,16 +229,31 @@ export default {
|
|
|
223
229
|
};
|
|
224
230
|
}
|
|
225
231
|
const { partages } = natureDesFichiers(lireJson(join(projet.sortie, 'remoteEntry.json')));
|
|
232
|
+
// Les politiques que le remote declare (lot 270) ; le BFF retire celui qui en exige une que sa
|
|
233
|
+
// CSP n'admet pas : l'etape le dit des la construction.
|
|
234
|
+
const declarees = projet.role === 'remote' ? (projet.source?.trustedTypes ?? []) : [];
|
|
235
|
+
const avertissements = declarees
|
|
236
|
+
.filter((politique) => !POLITIQUES_ADMISES.includes(politique))
|
|
237
|
+
.map(
|
|
238
|
+
(politique) =>
|
|
239
|
+
`trustedTypes : "${politique}" absente de la liste par defaut du BFF ` +
|
|
240
|
+
'(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
|
|
241
|
+
);
|
|
226
242
|
const defauts = [];
|
|
227
243
|
for (const chemin of livres) {
|
|
228
244
|
const nom = relatif(projet.sortie, chemin);
|
|
229
245
|
const contenu = /\.(?:m?js|html|svg|css)$/.test(nom) ? readFileSync(chemin, 'utf8') : '';
|
|
230
246
|
defauts.push(
|
|
231
|
-
...jugerUnFichierLivre(nom, contenu, {
|
|
247
|
+
...jugerUnFichierLivre(nom, contenu, {
|
|
248
|
+
role: projet.role,
|
|
249
|
+
partage: partages.has(nom),
|
|
250
|
+
declarees,
|
|
251
|
+
}),
|
|
232
252
|
);
|
|
233
253
|
}
|
|
234
254
|
return {
|
|
235
255
|
defauts,
|
|
256
|
+
avertissements,
|
|
236
257
|
resume: `${String(livres.length)} fichier(s) livre(s) dont ${String(partages.size)} du cadre partage, rien a signaler.`,
|
|
237
258
|
};
|
|
238
259
|
},
|
package/src/verifier.mjs
CHANGED
|
@@ -75,12 +75,14 @@ export async function verifier(
|
|
|
75
75
|
|
|
76
76
|
for (const [indice, etape] of retenues.entries()) {
|
|
77
77
|
journal.info(`[${String(indice + 1)}/${String(retenues.length)}] ${etape.nom}`);
|
|
78
|
-
if (etape.preparation === 'sprite' && !spritePose) {
|
|
79
|
-
journal.info(` sprite : ${poserLeSprite(projet)}`);
|
|
80
|
-
spritePose = true;
|
|
81
|
-
}
|
|
82
78
|
let resultat;
|
|
83
79
|
try {
|
|
80
|
+
// Un sprite qui ne se pose pas (absent, altere) est un defaut de l'etape qui le lit, nomme
|
|
81
|
+
// comme les autres, et non une commande qui ne peut pas s'executer.
|
|
82
|
+
if (etape.preparation === 'sprite' && !spritePose) {
|
|
83
|
+
journal.info(` sprite : ${poserLeSprite(projet)}`);
|
|
84
|
+
spritePose = true;
|
|
85
|
+
}
|
|
84
86
|
resultat = await etape.executer(projet, contexte);
|
|
85
87
|
} catch (erreur) {
|
|
86
88
|
resultat = { defauts: [erreur instanceof Error ? erreur.message : String(erreur)] };
|