@starterlib/cli 0.10.12 → 0.10.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,6 +41,7 @@ Scripts d'une SPA :
41
41
  | `starter-web precompresser` | Ecrit les variantes `.br` et `.gz`, et `precompression.index` (chaque variante et le condense de son source, que le serveur confronte) ; `starter-web precompresser <dossier>` pour un autre dossier |
42
42
  | `starter-web i18n:types` | Engendre `src/app/i18n/cles.ts`, le type des cles de chaque perimetre (`injecterTraduction<C>`) |
43
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 navigateurs` | Ecrit `.browserslistrc` : la liste browserslist du socle, tiree du plancher de la matrice (lot 276) |
44
45
  | `starter-web etapes` | Liste les etapes et les roles qu'elles concernent |
45
46
 
46
47
  Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peut pas s'executer
@@ -48,24 +49,27 @@ Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peu
48
49
 
49
50
  ## Les etapes de `verify`
50
51
 
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 |
52
+ | Etape | Role | Ce qu'elle garde |
53
+ | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `versions` | tous | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
55
+ | `navigateurs` | tous | La liste browserslist de la SPA (`.browserslistrc`, ou `browserslist` de `package.json`) est celle du plancher de la matrice : ni absente, ni plus basse, ni plus large (lot 276) |
56
+ | `dependances` | tous | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
57
+ | `jetons` | tous | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
58
+ | `format` | tous | `prettier --check .` |
59
+ | `lint` | tous | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
60
+ | `i18n` | tous | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, aide de chaque page (`<perimetre>.aide.<page>`), types a jour |
61
+ | `test` | tous | `ng test --no-watch` |
62
+ | `contrat` | tous | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
63
+ | `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 |
64
+ | `build` | tous | `starter-web construire` |
65
+ | `vulnerabilites` | tous | `pnpm audit --prod` : HIGH et CRITICAL bloquent sauf exception DATEE de `securite/exceptions-npm.json` (un trimestre au plus) ; SBOM CycloneDX dans `dist/sbom-front.cdx.json`, a cote du dossier livre (jamais servie) ; licences AGPL, SSPL et inconnues refusees (lot 139) |
66
+ | `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
67
+ | `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
68
+ | `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
69
+ | `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
70
+ | `autonome` | autonome | Aucune federation livree (`remoteEntry`, carte d'importation, `es-module-shims`) ; versions d'Angular et du socle egales a celles du jumeau (`starterWeb.jumeau`) ; poids de l'ecran (`starterWeb.ecran` : entree et seuil en Ko br), lu dans la carte de la construction (lot 240) |
71
+ | `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
72
+ | `securite` | tous | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
69
73
 
70
74
  `--etapes a,b` n'en joue que certaines ; `--sauter a,b` en retire.
71
75
 
@@ -80,8 +84,15 @@ par un point d'entree vide dans la configuration `production`).
80
84
 
81
85
  ## Ce que le CLI lit, et d'ou
82
86
 
83
- - le role : `package.json#starterWeb.role` (`"hote"` pour la coquille), sinon `remote` des qu'un
84
- `manifest.source.json` existe ;
87
+ - le role : `package.json#starterWeb.role` (`"hote"` pour la coquille, `"autonome"` pour une
88
+ application servie seule, sans federation : l'ecran anonyme d'identity, lot 240), sinon `remote` des
89
+ qu'un `manifest.source.json` existe ;
90
+ - pour une application autonome, dans `starterWeb` : `nom` (le projet d'`angular.json`),
91
+ `i18n.perimetres` (ses catalogues), `jumeau` (le dossier du remote du meme service, dont elle
92
+ partage les versions) et `ecran` (`{ "entree": "src/app/pages/connexion.ts", "seuilKo": 155 }`) ;
93
+ `starter-web construire` n'y ecrit pas de manifeste, et pose `--stats-json` ; `securite` y
94
+ neutralise UNE lecture de cookie du cadre (`DomAdapter.getCookie`, dans `main` faute de morceau
95
+ partage), toute autre restant un defaut ;
85
96
  - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
86
97
  - le prefixe des selecteurs : `prefix` du projet d'`angular.json` (ou `starterWeb.prefixe`) ;
87
98
  - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser` ;
@@ -19,7 +19,9 @@ proprietaire du 2026-09-26).
19
19
  | -------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
20
  | `@angular/core`, `common`, `compiler`, `compiler-cli`, `forms`, `platform-browser`, `router` | `22.2.0` | Premiere de la ligne 22.2, publiee le 2026-09-23 (plus de trois jours au 2026-09-27) |
21
21
  | `@angular/cdk`, `aria` | `22.2.0` | Le paquet `aria` est stable depuis la 22 ; il porte les primitives sans style. `@angular/material` est retire : aucun composant du socle ne l'importe |
22
+ | `@angular/elements` | `22.2.0` | Les custom elements de `@starterlib/elements` seulement (lot 299, decision du proprietaire du 2026-10-01) ; son pair exige le numero exact de `@angular/core`, d'ou 22.2.0 et non la 22.2.1 publiee |
22
23
  | `@angular/build`, `@angular/cli` | `22.2.0` | ⚠️ **Cadence differente de `core`.** L'outillage recoit des correctifs que le coeur ne recoit pas. Une matrice qui ne porterait qu'un seul numero mentirait des le premier correctif |
24
+ | `@angular-devkit/schematics` | `22.2.0` | Le moteur des schematics du CLI d'Angular, au meme numero : `@starterlib/schematics` (lot 285) n'en importe rien a l'execution, ses essais le lancent (`SchematicTestRunner`) |
23
25
  | `ng-packagr` | `22.2.1` | Derniere stable de la ligne 22.2 (2026-09-23) |
24
26
  | `typescript` | `6.0.3` | ⚠️ **Piege.** La 7.0.2 est publiee, mais `@angular/compiler-cli@22.2.0` exige `>=6.0 <6.1`. Installer la derniere casse la compilation |
25
27
  | `node` | `>=24.15.0` | 24.x LTS, declare dans `engines` ; le meme plancher que la construction Maven des SPA (`frontend.node.version`) |
@@ -31,6 +33,18 @@ proprietaire du 2026-09-26).
31
33
  | `stylelint`, `postcss-scss` | `17.15.0`, `4.0.9` | Les feuilles du socle et des SPA (lot 178, `@starterlib/eslint-config/stylelint`) ; `postcss-scss` lit le SCSS |
32
34
  | `jsdom`, `axe-core` | `30.0.1`, `4.13.0` | Le DOM des essais unitaires et le controle d'accessibilite : un correctif change ce qu'un essai voit |
33
35
 
36
+ ## Prochain train : `2026.2` (prevu, non applique)
37
+
38
+ Lot 297 : le train suivant se monte a la promotion LTS de Node 26 (calendrier de Node : fin octobre
39
+ 2026), et pas avant. Ce que la montee changera, dans un commit par depot (socle puis les quatre
40
+ interfaces), avec `pnpm verify` et `banc-d-assemblage.sh --construire` verts :
41
+
42
+ - `engines.node` exact sur Node 26, `setup-node` des workflows, `build-spa.yml` et
43
+ `frontend.node.version` de `platform-parent` ;
44
+ - pnpm 12 une fois publie et age de trois jours (`packageManager`, `frontend.pnpm.version`) ;
45
+ - reevaluation des API de la section 3 de `regles-front.md` (`withViewTransitions`, `debounced`,
46
+ `@boundary`, ancrage CSS avec le banc sur trois moteurs).
47
+
34
48
  ## A destination des depots de SPA (pas de ce depot)
35
49
 
36
50
  | Element | Version | Avertissement |
@@ -53,8 +67,33 @@ maximum, fonction par fonction, de ce que les feuilles et le noyau emploient san
53
67
  | `transition-behavior: allow-discrete` | 117 | 17.4 | 129 | admis pour une sortie qui anime `display` |
54
68
 
55
69
  Sous ce plancher, rien ne casse sans le dire : une surface sans `@starting-style` apparait d'un coup ; une
56
- couleur sans `light-dark()` n'existe pas, d'ou le plancher. Le banc tourne sur Chromium ; Firefox et WebKit
57
- entrent au banc d'assemblage (lot 276).
70
+ couleur sans `light-dark()` n'existe pas, d'ou le plancher.
71
+
72
+ Ce plancher est plus haut que celui d'Angular 22.2 (« baseline widely available » au 2026-05-07 : Chrome et
73
+ Edge 119, Firefox 119, Safari et iOS 17) : la construction ne vise pas plus bas que le socle n'en a besoin.
74
+
75
+ **Liste partagee (lot 276, 0.10.14).** La liste browserslist de chaque SPA DERIVE de la ligne du plancher
76
+ ci-dessus, que `@starterlib/cli` lit dans ce document publie avec lui : Chrome, Edge, Chrome Android et la
77
+ WebView d'Android au plancher de Chrome, Firefox et Firefox Android a celui de Firefox, Safari et iOS a
78
+ celui de Safari. `starter-web navigateurs` l'ecrit dans `.browserslistrc` ; l'etape `navigateurs` de
79
+ `starter-web verify` refuse une liste absente, plus basse ou plus large. Sous le plancher, l'hote montre
80
+ la page « navigateur non pris en charge » de `@starterlib/core/navigateur` au lieu de demarrer
81
+ (detection par fonction, jamais par l'agent utilisateur).
82
+
83
+ **Mesure (banc d'assemblage du BFF, lot 276, 2026-10-01).** Playwright 1.63 : Chromium 153, Firefox 155,
84
+ WebKit 26.6, Pixel 7 et iPhone 15 emules. Chromium et le telephone Android jouent tout le banc a chaque
85
+ poussee ; Firefox, WebKit et l'iPhone jouent les scenarios `@moteurs` et `@mobile` chaque semaine
86
+ (`BANC_MOTEURS=tous`).
87
+
88
+ | Moteur | Version mesuree | Plancher | Ce qui se degrade entre le plancher et la mesure |
89
+ | ------------- | --------------- | -------- | ---------------------------------------------------------------------------------- |
90
+ | Chromium/Edge | 153 | 123 | rien de connu : les types de confiance y sont appliques depuis bien avant 123 |
91
+ | Firefox | 155 | 129 | la protection des puits DOM par les types de confiance, si la version ne les a pas |
92
+ | Safari/iOS | 26.6 | 17.5 | idem ; la politique bloque toujours les scripts sans nonce |
93
+
94
+ **WebView.** Android System WebView suit Chromium, WKWebView suit le Safari du systeme : memes planchers
95
+ (decisions mobiles, ADR 0043 du socle `api/starter`). **Revision** : a chaque montee de Playwright (nouveaux moteurs) et au plus
96
+ tard a chaque majeure d'Angular.
58
97
 
59
98
  ## Regle de retard
60
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.10.12",
3
+ "version": "0.10.14",
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",
@@ -50,21 +50,28 @@ export function construire(
50
50
  { configuration = 'production', journal = console, sprite = true } = {},
51
51
  ) {
52
52
  if (sprite) journal.info(` sprite : ${poserLeSprite(projet)}`);
53
+ // Une application autonome ecrit la carte de sa construction : l'etape `autonome` y lit le
54
+ // morceau de la route de son ecran (lot 240).
55
+ const carte = projet.role === 'autonome' ? ['--stats-json'] : [];
53
56
  const code = executer(
54
57
  'pnpm',
55
- ['exec', 'ng', 'build', '--configuration', configuration],
58
+ ['exec', 'ng', 'build', '--configuration', configuration, ...carte],
56
59
  projet.racine,
57
60
  );
58
61
  if (code !== 0) {
59
62
  journal.erreur(` ng build --configuration ${configuration} : code ${String(code)}.`);
60
63
  return code;
61
64
  }
62
- if (projet.role === 'hote')
65
+ // L'hote et l'application autonome peignent un premier ecran : la police s'y precharge.
66
+ if (projet.role === 'hote' || projet.role === 'autonome')
63
67
  journal.info(` police prechargee : ${prechargerLaPolice(projet.sortie)}`);
64
- const manifeste = ecrireLeManifeste(projet);
65
- journal.info(
66
- ` manifest.json : ${manifeste.nom} ${manifeste.version}, socle ${manifeste.socle.construit}`,
67
- );
68
+ // Une application autonome n'est pas assemblee : aucun manifeste (lot 240).
69
+ if (projet.role !== 'autonome') {
70
+ const manifeste = ecrireLeManifeste(projet);
71
+ journal.info(
72
+ ` manifest.json : ${manifeste.nom} ${manifeste.version}, socle ${manifeste.socle.construit}`,
73
+ );
74
+ }
68
75
  const { resume, defauts } = juger(precompresser(projet.sortie));
69
76
  journal.info(` precompression : ${resume}`);
70
77
  for (const defaut of defauts) journal.erreur(` - ${defaut}`);
@@ -20,9 +20,14 @@ import { empreinteCourte } from '../commun.mjs';
20
20
 
21
21
  const SPRITE_HACHE = /^sprite-[A-Z0-9]{8}\.svg$/;
22
22
 
23
- /** Les chemins du sprite selon le role. */
23
+ /**
24
+ * Les chemins du sprite selon le role. Une application autonome (lot 240) peint le sprite du SOCLE,
25
+ * comme l'hote : elle n'a ni remote ni sprite metier.
26
+ */
27
+ const duSocle = (projet) => projet.role === 'hote' || projet.role === 'autonome';
28
+
24
29
  function reglages(projet) {
25
- if (projet.role === 'hote') {
30
+ if (duSocle(projet)) {
26
31
  return {
27
32
  source: join(
28
33
  projet.racine,
@@ -74,11 +79,11 @@ export function poserLeSprite(projet) {
74
79
  const { source, dossier, adresse, genere, constante } = reglages(projet);
75
80
  if (!existsSync(source)) {
76
81
  throw new Error(
77
- `sprite absent : ${source}${projet.role === 'hote' ? " (pnpm install d'abord)" : ''}.`,
82
+ `sprite absent : ${source}${duSocle(projet) ? " (pnpm install d'abord)" : ''}.`,
78
83
  );
79
84
  }
80
85
  const contenu = readFileSync(source);
81
- if (projet.role === 'hote') verifierLeSpriteDuSocle(source, contenu);
86
+ if (duSocle(projet)) verifierLeSpriteDuSocle(source, contenu);
82
87
  const fichier = `sprite-${empreinteCourte(contenu)}.svg`;
83
88
  mkdirSync(dossier, { recursive: true });
84
89
  for (const existant of readdirSync(dossier)) {
@@ -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
+ };
@@ -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) : [];
@@ -264,12 +264,21 @@ function lire(chemin) {
264
264
  }
265
265
  }
266
266
 
267
+ /**
268
+ * Les perimetres de catalogues d'un projet : ceux du manifeste (remote), ceux de
269
+ * `starterWeb.i18n.perimetres` (application autonome, lot 240), `socle` pour l'hote.
270
+ */
271
+ export function perimetresDuProjet(projet) {
272
+ if (projet.role === 'remote') return projet.source?.i18n?.perimetres ?? [];
273
+ if (projet.role === 'autonome') return projet.paquet.starterWeb?.i18n?.perimetres ?? ['socle'];
274
+ return ['socle'];
275
+ }
276
+
267
277
  export default {
268
278
  nom: 'i18n',
269
- roles: ['remote', 'hote'],
279
+ roles: ['remote', 'hote', 'autonome'],
270
280
  executer(projet) {
271
- const perimetres =
272
- projet.role === 'remote' ? (projet.source?.i18n?.perimetres ?? []) : ['socle'];
281
+ const perimetres = perimetresDuProjet(projet);
273
282
  if (perimetres.length === 0) {
274
283
  return { defauts: ['manifest.source.json : i18n.perimetres vide, aucun catalogue a lire.'] };
275
284
  }
@@ -305,8 +314,9 @@ export default {
305
314
  // L'aide contextuelle de chaque page du manifeste, et aucune aide sans page (lot 280).
306
315
  defauts.push(...jugerLesAides(catalogues, projet.source?.routes ?? []));
307
316
 
308
- // L'hote porte les cles que le noyau traduit par son catalogue.
309
- if (projet.role === 'hote') {
317
+ // L'hote, et l'application autonome qui pose le noyau comme lui, portent les cles que le noyau
318
+ // traduit par son catalogue.
319
+ if (projet.role === 'hote' || projet.role === 'autonome') {
310
320
  const dossier = dossierInstalle(projet.racine, '@starterlib/core');
311
321
  const exigees =
312
322
  dossier === null ? null : lireJson(join(dossier, 'schemas', 'cles-exigees-du-socle.json'));
@@ -67,7 +67,7 @@ export function jugerLesJetons(feuilles, definis) {
67
67
 
68
68
  export default {
69
69
  nom: 'jetons',
70
- roles: ['remote', 'hote'],
70
+ roles: ['remote', 'hote', 'autonome'],
71
71
  executer(projet) {
72
72
  const socle = join(projet.racine, 'node_modules', '@starterlib', 'tokens', 'styles');
73
73
  const definis = new Set();
@@ -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
+ };
@@ -44,7 +44,7 @@ function deleguer(projet, arguments_, resume) {
44
44
 
45
45
  export const format = {
46
46
  nom: 'format',
47
- roles: ['remote', 'hote'],
47
+ roles: ['remote', 'hote', 'autonome'],
48
48
  executer: (projet) =>
49
49
  deleguer(projet, ['exec', 'prettier', '--check', '.'], 'prettier --check . conforme.'),
50
50
  };
@@ -59,7 +59,7 @@ const CONFIGURATIONS_STYLELINT = [
59
59
 
60
60
  export const lint = {
61
61
  nom: 'lint',
62
- roles: ['remote', 'hote'],
62
+ roles: ['remote', 'hote', 'autonome'],
63
63
  preparation: 'sprite',
64
64
  executer(projet) {
65
65
  const sources = fichiers(join(projet.racine, 'src'))
@@ -95,7 +95,7 @@ export const lint = {
95
95
 
96
96
  export const test = {
97
97
  nom: 'test',
98
- roles: ['remote', 'hote'],
98
+ roles: ['remote', 'hote', 'autonome'],
99
99
  preparation: 'sprite',
100
100
  executer: (projet) => deleguer(projet, ['exec', 'ng', 'test', '--no-watch'], 'ng test conforme.'),
101
101
  };
@@ -136,7 +136,7 @@ export function motifsNonPortables(racine) {
136
136
  */
137
137
  export const contrat = {
138
138
  nom: 'contrat',
139
- roles: ['remote', 'hote'],
139
+ roles: ['remote', 'hote', 'autonome'],
140
140
  executer(projet) {
141
141
  if (projet.paquet.scripts?.['contrat:check'] === undefined) {
142
142
  return { sautee: 'aucun script contrat:check : pas de client engendre (ADR web 0008).' };
@@ -125,6 +125,15 @@ 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
+
128
137
  /** Les types d'un script EN LIGNE qui ne s'executent pas : des donnees (`esms-options`, cartes). */
129
138
  const SCRIPTS_DE_DONNEES = new Set([
130
139
  'esms-options',
@@ -164,13 +173,18 @@ function jugerUnePage(nom, contenu) {
164
173
 
165
174
  /**
166
175
  * Les defauts d'un fichier livre `nom` (relatif au dossier livre) de contenu `contenu`. `role` :
167
- * `remote` ou `hote` ; `partage` : le fichier est du code du cadre, partage par la federation.
176
+ * `remote`, `hote` ou `autonome` ; `partage` : le fichier est du code du cadre, partage par la
177
+ * federation ; `cadreNeutralise` : la lecture de cookie du cadre d'une application autonome a deja
178
+ * ete comptee, et se neutralise ici (une fois par fichier).
168
179
  */
169
180
  export function jugerUnFichierLivre(
170
181
  nom,
171
182
  contenu,
172
- { role = 'remote', partage = false, declarees = [] } = {},
183
+ { role = 'remote', partage = false, declarees = [], cadreNeutralise = false } = {},
173
184
  ) {
185
+ if (cadreNeutralise) {
186
+ contenu = contenu.replace(LECTURE_DE_COOKIE_DU_CADRE, 'getCookie($1){return null}');
187
+ }
174
188
  // Une carte, ou sa variante compressee : `/x.js.map.br` se servirait aussi bien que l'original.
175
189
  if (/\.map(?:\.br|\.gz)?$/.test(nom)) {
176
190
  return [`${nom} : carte de source livree (sourceMap doit rester false en production).`];
@@ -217,7 +231,7 @@ export function jugerUnFichierLivre(
217
231
 
218
232
  export default {
219
233
  nom: 'securite',
220
- roles: ['remote', 'hote'],
234
+ roles: ['remote', 'hote', 'autonome'],
221
235
  executer(projet) {
222
236
  // Les variantes precompressees derivent des fichiers lus ; celles d'une carte sont signalees.
223
237
  const livres = fichiers(projet.sortie).filter(
@@ -240,17 +254,32 @@ export default {
240
254
  '(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
241
255
  );
242
256
  const defauts = [];
257
+ // Application autonome : la lecture de cookie du cadre, une seule fois dans tout le livrable.
258
+ const autonome = projet.role === 'autonome';
259
+ let lecturesDuCadre = 0;
243
260
  for (const chemin of livres) {
244
261
  const nom = relatif(projet.sortie, chemin);
245
262
  const contenu = /\.(?:m?js|html|svg|css)$/.test(nom) ? readFileSync(chemin, 'utf8') : '';
263
+ const duCadre =
264
+ autonome && /\.m?js$/.test(nom)
265
+ ? (contenu.match(LECTURE_DE_COOKIE_DU_CADRE)?.length ?? 0)
266
+ : 0;
267
+ lecturesDuCadre += duCadre;
246
268
  defauts.push(
247
269
  ...jugerUnFichierLivre(nom, contenu, {
248
270
  role: projet.role,
249
271
  partage: partages.has(nom),
250
272
  declarees,
273
+ cadreNeutralise: duCadre === 1,
251
274
  }),
252
275
  );
253
276
  }
277
+ if (lecturesDuCadre > 1) {
278
+ defauts.push(
279
+ `lecture de cookie du cadre : ${String(lecturesDuCadre)} occurrences (DomAdapter.getCookie) ; ` +
280
+ 'une seule est admise dans une application autonome.',
281
+ );
282
+ }
254
283
  return {
255
284
  defauts,
256
285
  avertissements,
@@ -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,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Etape `vulnerabilites` (lot 139) : `pnpm audit --prod` bloquant sur HIGH et CRITICAL, sauf
3
+ * exception datee de `securite/exceptions-npm.json` ; SBOM CycloneDX des dependances de production
4
+ * dans `dist/sbom-front.cdx.json` (a cote du dossier livre, jamais servie) ; licences AGPL, SSPL et
5
+ * inconnues refusees. Le registre npm doit repondre : sans lui, on refuse plutot que supposer.
6
+ */
7
+ import { auditerLesDependances } from '../vulnerabilites.mjs';
8
+
9
+ export default {
10
+ nom: 'vulnerabilites',
11
+ roles: ['remote', 'hote', 'autonome'],
12
+ executer(projet) {
13
+ try {
14
+ const { defauts, exceptes, composants } = auditerLesDependances(projet.racine);
15
+ const sous = exceptes.length > 0 ? `, ${String(exceptes.length)} avis excepte(s)` : '';
16
+ return {
17
+ defauts,
18
+ resume: `${String(composants)} composant(s) de production audites et sous licence admise${sous} (dist/sbom-front.cdx.json).`,
19
+ };
20
+ } catch (erreur) {
21
+ return { defauts: [erreur instanceof Error ? erreur.message : String(erreur)] };
22
+ }
23
+ },
24
+ };
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
@@ -12,9 +12,11 @@ import { poserLeSprite } from './construction/sprite.mjs';
12
12
  import { ecrireLesSimulations } from './construction/simulations.mjs';
13
13
  import { ecrireLesTypesDeCles } from './construction/types-i18n.mjs';
14
14
  import { lireJson, relatif } from './commun.mjs';
15
+ import { ecrireLesNavigateurs } from './navigateurs.mjs';
15
16
  import { versionDuSocle } from './matrice.mjs';
16
17
  import { ProjetIllisible, lireLeProjet } from './projet.mjs';
17
18
  import { ETAPES, etapesInconnues, verifier } from './verifier.mjs';
19
+ import { perimetresDuProjet } from './etapes/i18n.mjs';
18
20
  import { join, resolve } from 'node:path';
19
21
 
20
22
  const AIDE = `starter-web : la chaine d'une SPA du socle frontal (@starterlib/cli).
@@ -29,6 +31,7 @@ Commandes :
29
31
  i18n:types engendre le type des cles de chaque perimetre (src/app/i18n/cles.ts)
30
32
  simulations recopie les stubs WireMock du service pour l'hote de developpement
31
33
  (src/hote-dev/simulations.engendre.json)
34
+ navigateurs ecrit la liste browserslist du socle (.browserslistrc, plancher de la matrice)
32
35
  etapes liste les etapes de verify
33
36
 
34
37
  Options de verify :
@@ -196,9 +199,13 @@ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
196
199
  }
197
200
  case 'precompresser':
198
201
  return precompresserEtDire(projet.sortie, sortie);
202
+ case 'navigateurs': {
203
+ const chemin = ecrireLesNavigateurs(projet.racine);
204
+ sortie.info(`navigateurs : ${relatif(projet.racine, chemin)} (plancher du socle)`);
205
+ return 0;
206
+ }
199
207
  case 'i18n:types': {
200
- const perimetres =
201
- projet.role === 'remote' ? (projet.source?.i18n?.perimetres ?? []) : ['socle'];
208
+ const perimetres = perimetresDuProjet(projet);
202
209
  const catalogues = Object.fromEntries(
203
210
  perimetres.map((perimetre) => [
204
211
  perimetre,
@@ -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), sinon `remote` des qu'un
9
- * `manifest.source.json` existe ;
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(`starterWeb.role : "remote" ou "hote", recu "${String(role)}".`);
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) {
package/src/verifier.mjs CHANGED
@@ -2,14 +2,15 @@
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 -> simulations
6
- * (remote) -> build -> manifeste -> federation -> entree (hote) -> budget -> amorcage (hote) ->
7
- * securite.
5
+ * versions -> navigateurs -> dependances -> jetons -> format -> lint -> i18n -> test -> contrat -> simulations
6
+ * (remote) -> build -> manifeste -> federation -> entree (hote) -> budget -> autonome (autonome) ->
7
+ * amorcage (hote) -> securite.
8
8
  *
9
9
  * Le sprite est pose une fois, avant la premiere etape qui lit le code (lint, test, build).
10
10
  */
11
11
  import { construire } from './construction/construire.mjs';
12
12
  import { poserLeSprite } from './construction/sprite.mjs';
13
+ import autonome from './etapes/autonome.mjs';
13
14
  import budget from './etapes/budget.mjs';
14
15
  import dependances from './etapes/dependances.mjs';
15
16
  import federation from './etapes/federation.mjs';
@@ -17,14 +18,16 @@ import { amorcage, entree } from './etapes/hote.mjs';
17
18
  import i18n from './etapes/i18n.mjs';
18
19
  import jetons from './etapes/jetons.mjs';
19
20
  import manifeste from './etapes/manifeste.mjs';
21
+ import navigateurs from './etapes/navigateurs.mjs';
20
22
  import { contrat, format, lint, test } from './etapes/outillage.mjs';
21
23
  import securite from './etapes/securite.mjs';
22
24
  import simulations from './etapes/simulations.mjs';
23
25
  import versions from './etapes/versions.mjs';
26
+ import vulnerabilites from './etapes/vulnerabilites.mjs';
24
27
 
25
28
  const build = {
26
29
  nom: 'build',
27
- roles: ['remote', 'hote'],
30
+ roles: ['remote', 'hote', 'autonome'],
28
31
  preparation: 'sprite',
29
32
  executer(projet, contexte) {
30
33
  const code = construire(projet, { journal: contexte.journal, sprite: false });
@@ -37,6 +40,7 @@ const build = {
37
40
  /** Les etapes, dans leur ordre. */
38
41
  export const ETAPES = [
39
42
  versions,
43
+ navigateurs,
40
44
  dependances,
41
45
  jetons,
42
46
  format,
@@ -46,10 +50,12 @@ export const ETAPES = [
46
50
  contrat,
47
51
  simulations,
48
52
  build,
53
+ vulnerabilites,
49
54
  manifeste,
50
55
  federation,
51
56
  entree,
52
57
  budget,
58
+ autonome,
53
59
  amorcage,
54
60
  securite,
55
61
  ];
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Vulnerabilites et licences des dependances npm (lot 139), partagees par le socle frontal
3
+ * (`tools/verifier-dependances.mjs`) et chaque SPA (etape `vulnerabilites` de `starter-web verify`).
4
+ *
5
+ * - `pnpm audit --prod` : tout avis HIGH ou CRITICAL bloque, sauf exception DATEE du registre
6
+ * `securite/exceptions-npm.json` ; une exception sans date, perimee ou a plus d'un trimestre
7
+ * bloque aussi (politique commune : api/starter docs/security/gestion-des-vulnerabilites.md) ;
8
+ * - `pnpm sbom` : la SBOM CycloneDX des dependances de production, ecrite dans
9
+ * `dist/sbom-front.cdx.json`, A COTE du dossier livre (jamais servie : elle dirait a un attaquant
10
+ * chaque version installee) ;
11
+ * - licences : AGPL, SSPL ou inconnue interdites dans les dependances de production.
12
+ */
13
+ import { spawnSync } from 'node:child_process';
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+
17
+ /** Une exception ne vaut qu'un trimestre : au-dela, elle se renouvelle, justification relue. */
18
+ export const HORIZON_JOURS = 92;
19
+ const GRAVES = new Set(['high', 'critical']);
20
+ const IDENTIFIANT = /^(GHSA(-[23456789cfghjmpqrvwx]{4}){3}|CVE-\d{4}-\d{4,})$/;
21
+ const LICENCE_INTERDITE = /\b(AGPL|SSPL)/i;
22
+
23
+ const jour = (date) => date.toISOString().slice(0, 10);
24
+
25
+ /** Lit le registre : les exceptions valables, et les defauts de celles qui ne le sont pas. */
26
+ export function lireExceptions(registre, aujourdHui = new Date()) {
27
+ const defauts = [];
28
+ const valables = [];
29
+ const limite = new Date(aujourdHui.getTime() + HORIZON_JOURS * 86_400_000);
30
+ for (const [indice, exception] of (registre.exceptions ?? []).entries()) {
31
+ const nom = `exception ${String(indice + 1)} (${String(exception.avis)})`;
32
+ if (typeof exception.avis !== 'string' || !IDENTIFIANT.test(exception.avis)) {
33
+ defauts.push(`${nom} : avis absent ou illisible (GHSA-xxxx-xxxx-xxxx ou CVE-AAAA-NNNN)`);
34
+ } else if (
35
+ typeof exception.justification !== 'string' ||
36
+ exception.justification.trim().length < 20
37
+ ) {
38
+ defauts.push(`${nom} : justification absente ou trop courte`);
39
+ } else if (
40
+ typeof exception.echeance !== 'string' ||
41
+ !/^\d{4}-\d{2}-\d{2}$/.test(exception.echeance) ||
42
+ Number.isNaN(Date.parse(exception.echeance))
43
+ ) {
44
+ defauts.push(`${nom} : sans echeance datee (AAAA-MM-JJ)`);
45
+ } else if (exception.echeance < jour(aujourdHui)) {
46
+ defauts.push(`${nom} : perimee depuis le ${exception.echeance}`);
47
+ } else if (exception.echeance > jour(limite)) {
48
+ defauts.push(
49
+ `${nom} : echeance a plus de ${String(HORIZON_JOURS)} jours (${exception.echeance})`,
50
+ );
51
+ } else {
52
+ valables.push(exception);
53
+ }
54
+ }
55
+ return { valables, defauts };
56
+ }
57
+
58
+ /** Les avis graves de `pnpm audit --json` : bloquants, ou couverts par une exception valable. */
59
+ export function trierLesAvis(audit, exceptions) {
60
+ const bloquants = [];
61
+ const exceptes = [];
62
+ for (const avis of Object.values(audit.advisories ?? {})) {
63
+ if (!GRAVES.has(String(avis.severity).toLowerCase())) continue;
64
+ const identifiants = [avis.github_advisory_id, ...(avis.cves ?? [])].filter(Boolean);
65
+ const exception = exceptions.find((candidate) => identifiants.includes(candidate.avis));
66
+ const ligne =
67
+ `${String(avis.severity).toUpperCase()} ${String(avis.module_name)} ` +
68
+ `${identifiants.join(' ')} (corrige : ${String(avis.patched_versions)})`;
69
+ if (exception === undefined) bloquants.push(ligne);
70
+ else exceptes.push(`${ligne}, excepte jusqu'au ${String(exception.echeance)}`);
71
+ }
72
+ return { bloquants, exceptes };
73
+ }
74
+
75
+ /** Les composants de la SBOM dont la licence est interdite ou inconnue. */
76
+ export function licencesRefusees(sbom) {
77
+ const refus = [];
78
+ for (const composant of sbom.components ?? []) {
79
+ const licences = (composant.licenses ?? [])
80
+ .map((entree) => entree.expression ?? entree.license?.id ?? entree.license?.name)
81
+ .filter((licence) => typeof licence === 'string' && licence.trim() !== '');
82
+ const nom =
83
+ `${composant.group ? `${String(composant.group)}/` : ''}` +
84
+ `${String(composant.name)}@${String(composant.version)}`;
85
+ if (licences.length === 0) refus.push(`${nom} : licence inconnue`);
86
+ else if (licences.some((licence) => LICENCE_INTERDITE.test(licence))) {
87
+ refus.push(`${nom} : licence interdite (${licences.join(', ')})`);
88
+ }
89
+ }
90
+ return refus;
91
+ }
92
+
93
+ /** `pnpm` en sortie capturee ; sous Windows, un script `.cmd`, d'ou l'interpreteur. */
94
+ function pnpm(arguments_, racine) {
95
+ const resultat = spawnSync('pnpm', arguments_, {
96
+ cwd: racine,
97
+ encoding: 'utf8',
98
+ maxBuffer: 64 * 1024 * 1024,
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ shell: process.platform === 'win32',
101
+ });
102
+ return {
103
+ code: resultat.status ?? 2,
104
+ stdout: resultat.stdout ?? '',
105
+ stderr: resultat.stderr ?? '',
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Audit, SBOM et licences d'un projet pnpm. Rend `{ defauts, exceptes, composants, sbom }` ;
111
+ * `sbom` est le chemin ecrit. Leve une erreur quand le registre ne repond pas (on refuse plutot
112
+ * que supposer).
113
+ */
114
+ export function auditerLesDependances(racine, { lancer = pnpm, aujourdHui = new Date() } = {}) {
115
+ const chemin = join(racine, 'securite/exceptions-npm.json');
116
+ const registre = existsSync(chemin) ? JSON.parse(readFileSync(chemin, 'utf8')) : {};
117
+ const { valables, defauts } = lireExceptions(registre, aujourdHui);
118
+
119
+ // `pnpm audit` rend 1 quand il trouve des avis : seule une sortie illisible empeche de juger.
120
+ const audit = lancer(['audit', '--prod', '--json'], racine);
121
+ let rapport;
122
+ try {
123
+ rapport = JSON.parse(audit.stdout);
124
+ } catch {
125
+ throw new Error(`pnpm audit illisible (registre injoignable ?) : ${audit.stderr.trim()}`);
126
+ }
127
+ const { bloquants, exceptes } = trierLesAvis(rapport, valables);
128
+ defauts.push(...bloquants.map((ligne) => `avis non excepte : ${ligne}`));
129
+
130
+ const sbom = lancer(['sbom', '--sbom-format', 'cyclonedx', '--prod'], racine);
131
+ if (sbom.code !== 0) throw new Error(`pnpm sbom a echoue : ${sbom.stderr.trim()}`);
132
+ const document = JSON.parse(sbom.stdout);
133
+ mkdirSync(join(racine, 'dist'), { recursive: true });
134
+ const sortie = join(racine, 'dist/sbom-front.cdx.json');
135
+ writeFileSync(sortie, `${JSON.stringify(document, null, 2)}\n`);
136
+ defauts.push(...licencesRefusees(document));
137
+
138
+ return { defauts, exceptes, composants: document.components?.length ?? 0, sbom: sortie };
139
+ }