@starterlib/cli 0.11.3 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,6 +95,11 @@ par un point d'entree vide dans la configuration `production`).
95
95
  `starter-web construire` n'y ecrit pas de manifeste, et pose `--stats-json` ; `securite` y
96
96
  neutralise UNE lecture de cookie du cadre (`DomAdapter.getCookie`, dans `main` faute de morceau
97
97
  partage), toute autre restant un defaut ;
98
+ - pour une application autonome PRERENDUE (0.11.4), `starterWeb.prerendu: true` : un site statique servi a la
99
+ racine de son domaine (`outputMode: "static"`), que le navigateur ne reprend pas. `starter-web construire` sort
100
+ de chaque page les feuilles des composants et les attributs `style` dans `composants-<empreinte>.css` (une
101
+ politique de securite stricte les refuserait en ligne) et y precharge la police ; son sprite est servi a
102
+ `/assets/icones/` (`src/construction/prerendu.mjs`) ;
98
103
  - pour l'hote, dans `starterWeb` : `premierEcran.entrees` (les sources de ses gabarits et de sa page d'arrivee,
99
104
  `["src/app/gabarits/coquille.ts", "src/app/pages/accueil.ts"]`), cherchees dans `browser-stats.json` que la
100
105
  cible esbuild d'`angular.json` ecrit avec `statsJson: true` ; sans elles, le premier ecran compte toutes les pages
@@ -1,6 +1,7 @@
1
1
  # Matrice de compatibilite du socle frontal
2
2
 
3
- Verifiee le 2026-10-07 contre le registre npm (montee au dernier correctif de la ligne 22.2 ; avant elle, lot 154). Toute montee de version se fait
3
+ Verifiee le 2026-10-11 contre le registre npm (montee au correctif 22.2.2 d'Angular, 0.11.5 ; avant elle, 22.2.1 le
4
+ 2026-10-07 et lot 154). Toute montee de version se fait
4
5
  **ligne par ligne, dans le meme commit**, et `pnpm versions:check` echoue si le
5
6
  `package.json` s'en ecarte (version non exacte, autre que celle de la matrice, paquet Angular absent
6
7
  de la matrice, greffon de federation d'une autre majeure.mineure qu'Angular, autre pnpm, Node sous le
@@ -9,30 +10,32 @@ lit CE document, publie avec lui, et ses essais le comparent a la matrice de `ve
9
10
 
10
11
  ⚠️ **Trois jours d'anciennete au moins** : `minimumReleaseAge: 4320` (`pnpm-workspace.yaml`) tient hors
11
12
  du verrou toute version publiee depuis moins de trois jours (constat A1405). Une version plus recente,
12
- meme corrective, attend : au 2026-10-07, `axe-core` 4.14.0 (2026-10-05), `typescript-eslint` 8.71.1
13
- (2026-10-05), `jsdom` 30.1.2 (2026-10-04), `@microsoft/api-extractor` 7.59.4 (2026-10-06) et `orval`
14
- 8.40.0 (2026-10-04) n'entrent pas ; la version precedente de chacun y entre. Angular monte en 22.2.1 le
15
- 2026-10-07 (correctif publie le 2026-09-30), apres la 22.2.0 du 2026-09-27.
13
+ meme corrective, attend : au 2026-10-11, `angular-eslint` 22.6.0 (2026-10-11), `prettier` 3.9.10 (2026-10-10)
14
+ et `@types/node` 24.19.2 (2026-10-09) n'entrent pas ; la version precedente de chacun reste. Angular monte en
15
+ 22.2.2 le 2026-10-11 (correctif publie le 2026-10-08 a 17 h 55 UTC, entre au verrou trois jours plus tard ; son
16
+ outillage, publie le 2026-10-07, le precede). `axe-core` 4.14.0 (2026-10-05) attend `@axe-core/playwright` 4.14 :
17
+ les deux portent le meme numero (section des depots de SPA), et le second n'existe qu'en 4.13.0.
16
18
 
17
19
  ## Train `2026.1`
18
20
 
19
- | Element | Version | Pourquoi cette version exactement |
20
- | -------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | `@angular/core`, `common`, `compiler`, `compiler-cli`, `forms`, `platform-browser`, `router` | `22.2.1` | Dernier correctif de la ligne 22.2, publie le 2026-09-30 (plus de trois jours au 2026-10-07) |
22
- | `@angular/cdk`, `aria` | `22.2.1` | 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 |
23
- | `@angular/elements` | `22.2.1` | 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 le meme 22.2.1 |
24
- | `@angular/build`, `@angular/cli` | `22.2.1` | ⚠️ **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 |
25
- | `@angular-devkit/schematics` | `22.2.1` | 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`) |
26
- | `ng-packagr` | `22.2.4` | Derniere stable de la ligne 22.2 (2026-10-01) |
27
- | `typescript` | `6.0.3` | ⚠️ **Piege.** La 7.0.2 est publiee, mais `@angular/compiler-cli@22.2.1` exige `>=6.0 <6.1`. Installer la derniere casse la compilation |
28
- | `node` | `>=24.15.0` | 24.x LTS, declare dans `engines` ; le meme plancher que la construction Maven des SPA (`frontend.node.version`) |
29
- | `pnpm` | `11.27.1` | Declare dans `packageManager` ; reglages dans `pnpm-workspace.yaml` (`allowBuilds`, `minimumReleaseAge`). Le meme pour la construction Maven des SPA (`frontend.pnpm.version` de `platform-parent`) |
30
- | `vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` | `5.0.3` | Lanceur officiel du constructeur `@angular/build:unit-test` ; un meme numero pour toute la famille (des numeros meles chargent deux copies du lanceur) |
31
- | `playwright`, `@playwright/test` | `1.63.0` | Le navigateur des essais : un correctif change un rendu mesure au pixel |
32
- | `eslint` | `10.12.0` | `angular-eslint@22.5.0` accepte les majeurs 9 et 10 |
33
- | `angular-eslint` | `22.5.0` | Suit le majeur d'Angular |
34
- | `stylelint`, `postcss-scss` | `17.16.0`, `4.0.9` | Les feuilles du socle et des SPA (lot 178, `@starterlib/eslint-config/stylelint`) ; `postcss-scss` lit le SCSS |
35
- | `jsdom`, `axe-core` | `30.1.1`, `4.13.0` | Le DOM des essais unitaires et le controle d'accessibilite : un correctif change ce qu'un essai voit |
21
+ | Element | Version | Pourquoi cette version exactement |
22
+ | -------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `@angular/core`, `common`, `compiler`, `compiler-cli`, `forms`, `platform-browser`, `router` | `22.2.2` | Dernier correctif de la ligne 22.2, publie le 2026-10-08 (trois jours revolus le 2026-10-11) |
24
+ | `@angular/cdk`, `aria` | `22.2.2` | 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 |
25
+ | `@angular/elements` | `22.2.2` | 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 le meme 22.2.2 |
26
+ | `@angular/ssr`, `platform-server` | `22.2.2` | Le prerendu d'une application autonome (`starterWeb.prerendu`, 0.11.4 : le site public d'Aplomb) : la construction seule s'en sert, aucun serveur n'est livre. Le meme numero que `@angular/core`, dont leurs pairs exigent l'exact |
27
+ | `@angular/build`, `@angular/cli` | `22.2.2` | ⚠️ **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 |
28
+ | `@angular-devkit/schematics` | `22.2.2` | 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`) |
29
+ | `ng-packagr` | `22.2.4` | Derniere stable de la ligne 22.2 (2026-10-01) |
30
+ | `typescript` | `6.0.3` | ⚠️ **Piege.** La 7.0.2 est publiee, mais `@angular/compiler-cli@22.2.2` exige `>=6.0 <6.1`. Installer la derniere casse la compilation |
31
+ | `node` | `>=24.15.0` | 24.x LTS, declare dans `engines` ; le meme plancher que la construction Maven des SPA (`frontend.node.version`) |
32
+ | `pnpm` | `11.27.1` | Declare dans `packageManager` ; reglages dans `pnpm-workspace.yaml` (`allowBuilds`, `minimumReleaseAge`). Le meme pour la construction Maven des SPA (`frontend.pnpm.version` de `platform-parent`) |
33
+ | `vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` | `5.0.3` | Lanceur officiel du constructeur `@angular/build:unit-test` ; un meme numero pour toute la famille (des numeros meles chargent deux copies du lanceur) |
34
+ | `playwright`, `@playwright/test` | `1.64.0` | Le navigateur des essais : un correctif change un rendu mesure au pixel |
35
+ | `eslint` | `10.12.0` | `angular-eslint@22.5.0` accepte les majeurs 9 et 10 |
36
+ | `angular-eslint` | `22.5.0` | Suit le majeur d'Angular |
37
+ | `stylelint`, `postcss-scss` | `17.16.0`, `4.0.9` | Les feuilles du socle et des SPA (lot 178, `@starterlib/eslint-config/stylelint`) ; `postcss-scss` lit le SCSS |
38
+ | `jsdom`, `axe-core` | `30.1.2`, `4.13.0` | Le DOM des essais unitaires et le controle d'accessibilite : un correctif change ce qu'un essai voit |
36
39
 
37
40
  ## Prochain train : `2026.2` (prevu, non applique)
38
41
 
@@ -51,7 +54,7 @@ interfaces), avec `pnpm verify` et `banc-d-assemblage.sh --construire` verts :
51
54
  | Element | Version | Avertissement |
52
55
  | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
56
  | `@angular-architects/native-federation` | `22.2.2` | ⚠️ **A epingler a l'exact, jamais par etiquette.** Le 2026-09-12, l'etiquette `latest` du paquet pointait sur la **21.2.6**, publiee apres la 22.1.2. Un `pnpm add` sans version ramene donc un majeur de retard, en silence, et l'echec se manifeste au chargement d'un remote, loin de la cause. Version mesuree au spike du 2026-09-16. Sa majeure.mineure est celle d'Angular (22.2) : `versions:check` refuse un couple disjoint, et les deux montent dans le meme commit (235a) |
54
- | `orval` | `8.39.0` | Client d'API engendre depuis le contrat du service (ADR web 0008) : services `HttpClient` pour les ecritures, `httpResource` pour les lectures, aucun schema zod, donc aucune dependance a l'execution. Derniere stable de plus de trois jours au 2026-10-07 (8.39.0, publiee le 2026-09-30) ; a epingler a l'exact, en dependance de developpement |
57
+ | `orval` | `8.41.0` | Client d'API engendre depuis le contrat du service (ADR web 0008) : services `HttpClient` pour les ecritures, `httpResource` pour les lectures, aucun schema zod, donc aucune dependance a l'execution. Derniere stable de plus de trois jours au 2026-10-07 (8.39.0, publiee le 2026-09-30) ; a epingler a l'exact, en dependance de developpement |
55
58
  | `es-module-shims` | `2.8.4` | Dependance du greffon, et pas un detail : c'est ELLE qui cree la politique de types de confiance `es-module-shims`. Toute montee doit verifier que le nom de la politique n'a pas change, sinon la page devient blanche sans erreur serveur |
56
59
  | `@axe-core/playwright` | `4.13.0` | Les essais d'accessibilite de bout en bout de la coquille (Playwright) ; le meme numero qu'`axe-core`, qui fait le controle : deux moteurs d'une version differente ne signaleraient pas les memes violations |
57
60
 
@@ -78,9 +81,11 @@ Ce plancher est plus haut que celui d'Angular 22.2 (« baseline widely available
78
81
  Edge 119, Firefox 119, Safari et iOS 17) : la construction ne vise pas plus bas que le socle n'en a besoin.
79
82
 
80
83
  **Liste partagee (lot 276, 0.10.14).** La liste browserslist de chaque SPA DERIVE de la ligne du plancher
81
- ci-dessus, que `@starterlib/cli` lit dans ce document publie avec lui : Chrome, Edge, Chrome Android et la
82
- WebView d'Android au plancher de Chrome, Firefox et Firefox Android a celui de Firefox, Safari et iOS a
83
- celui de Safari. `starter-web navigateurs` l'ecrit dans `.browserslistrc` ; l'etape `navigateurs` de
84
+ ci-dessus, que `@starterlib/cli` lit dans ce document publie avec lui : Chrome, Edge et Chrome Android au
85
+ plancher de Chrome, Firefox et Firefox Android a celui de Firefox, Safari et iOS a celui de Safari. La WebView
86
+ d'Android, qui suit Chromium, n'a pas de ligne a elle depuis la 0.11.5 : browserslist ne la connait qu'a sa
87
+ derniere version et la politique d'Angular (« baseline widely available ») ne la nomme pas, si bien que chaque
88
+ construction avertissait d'un navigateur hors de sa prise en charge ; la cible produite ne change pas. `starter-web navigateurs` l'ecrit dans `.browserslistrc` ; l'etape `navigateurs` de
84
89
  `starter-web verify` refuse une liste absente, plus basse ou plus large. Sous le plancher, l'hote montre
85
90
  la page « navigateur non pris en charge » de `@starterlib/core/navigateur` au lieu de demarrer
86
91
  (detection par fonction, jamais par l'agent utilisateur).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starterlib/cli",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "Chaine de verification et de construction des SPA du socle frontal starter : starter-web verify, construire, sprite, manifeste, precompresser. Jamais en dependance de production.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/src/commun.mjs CHANGED
@@ -32,9 +32,21 @@ export function lireJson(chemin) {
32
32
  }
33
33
  }
34
34
 
35
- /** Des octets en kilo-octets, au dixieme. */
35
+ /** Des octets en kilo-octets, au dixieme : pour LIRE une mesure, jamais pour la juger (voir `depasse`). */
36
36
  export const ko = (octets) => Math.round((octets / 1024) * 10) / 10;
37
37
 
38
+ /**
39
+ * Vrai quand `octets` depassent `seuilKo` kilo-octets (de 1 024 octets), compare en OCTETS exacts (0.11.5). Juge sur
40
+ * `ko`, un depassement de moins de 52 octets se lisait au seuil et passait : le 2026-10-11, l'ecran de connexion
41
+ * d'Aplomb pesait 166 951 octets pour un seuil de 163 Ko (166 912 octets), lu « 163 Ko br », et l'etape le
42
+ * declarait conforme.
43
+ */
44
+ export const depasse = (octets, seuilKo) => octets > seuilKo * 1024;
45
+
46
+ /** Une mesure jugee contre son seuil, lisible et exacte : `163 Ko br (166951 o) > 163 Ko br (166912 o)`. */
47
+ export const mesureEtSeuil = (octets, seuilKo) =>
48
+ `${String(ko(octets))} Ko br (${String(octets)} o) > ${String(seuilKo)} Ko br (${String(seuilKo * 1024)} o)`;
49
+
38
50
  /**
39
51
  * Empreinte courte d'un contenu, dans la forme que le socle Java reconnait comme « hachee » (huit
40
52
  * caracteres alphanumeriques apres un tiret) : un actif ainsi nomme est servi immuable un an.
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * 1. le sprite hache et sa constante (le code l'importe) ;
5
5
  * 2. `ng build` dans la configuration demandee ;
6
- * 3. l'hote seul : le prechargement de la police du premier ecran ;
6
+ * 3. l'hote et l'application autonome : le prechargement de la police du premier ecran ; une application
7
+ * PRERENDUE (`starterWeb.prerendu`, 0.11.4) le pose dans chaque page, et sort de ses pages les feuilles des
8
+ * composants et les attributs `style` (`prerendu.mjs`) ;
7
9
  * 4. le manifeste (il renomme les catalogues i18n) ;
8
10
  * 5. la precompression (apres le renommage, sans quoi des variantes resteraient orphelines).
9
11
  */
@@ -12,6 +14,11 @@ import { join } from 'node:path';
12
14
  import { executer } from '../commun.mjs';
13
15
  import { ecrireLeManifeste } from './manifeste.mjs';
14
16
  import { juger, precompresser } from './precompresser.mjs';
17
+ import {
18
+ externaliserLesStyles,
19
+ prechargerLaPoliceDansLesPages,
20
+ resumerLExternalisation,
21
+ } from './prerendu.mjs';
15
22
  import { poserLeSprite } from './sprite.mjs';
16
23
 
17
24
  /** Inter latin, graisse variable, style normal : le nom que `polices.css` du socle donne. */
@@ -44,6 +51,16 @@ export function prechargerLaPolice(sortie) {
44
51
  return polices[0];
45
52
  }
46
53
 
54
+ /**
55
+ * Une construction LIVREE : celle de production. Une autre configuration (`development`, pour l'hote de developpement)
56
+ * ecrit dans son propre dossier (son `outputPath`), ses actifs sous `ui/<nom>/` : ni police prechargee, ni manifeste,
57
+ * ni precompression, que ces etapes chercheraient dans le dossier de production (`pnpm build:dev` d'un remote echouait
58
+ * sur une copie neuve : « remoteEntry.json absent », constate le 2026-10-10).
59
+ */
60
+ export function estLivree(configuration) {
61
+ return configuration === 'production';
62
+ }
63
+
47
64
  /** Construit ; rend 0, ou le code de l'etape qui a echoue. `journal` : `{ info, erreur }`. */
48
65
  export function construire(
49
66
  projet,
@@ -62,9 +79,20 @@ export function construire(
62
79
  journal.erreur(` ng build --configuration ${configuration} : code ${String(code)}.`);
63
80
  return code;
64
81
  }
65
- // L'hote et l'application autonome peignent un premier ecran : la police s'y precharge.
66
- if (projet.role === 'hote' || projet.role === 'autonome')
82
+ if (!estLivree(configuration)) {
83
+ journal.info(
84
+ ` ${configuration} : construction non livree, sans police, manifeste ni precompression.`,
85
+ );
86
+ return 0;
87
+ }
88
+ // L'hote et l'application autonome peignent un premier ecran : la police s'y precharge. Une application prerendue
89
+ // a une page par adresse : chacune la precharge, et ses styles sortent de la page (politique stricte).
90
+ if (projet.prerendu) {
91
+ journal.info(` police prechargee : ${prechargerLaPoliceDansLesPages(projet.sortie)}`);
92
+ journal.info(` prerendu : ${resumerLExternalisation(externaliserLesStyles(projet.sortie))}`);
93
+ } else if (projet.role === 'hote' || projet.role === 'autonome') {
67
94
  journal.info(` police prechargee : ${prechargerLaPolice(projet.sortie)}`);
95
+ }
68
96
  // Une application autonome n'est pas assemblee : aucun manifeste (lot 240).
69
97
  if (projet.role !== 'autonome') {
70
98
  const manifeste = ecrireLeManifeste(projet);
@@ -0,0 +1,202 @@
1
+ /**
2
+ * La sortie d'une application autonome PRERENDUE (`starterWeb.prerendu`, 0.11.4) : un site statique, une page HTML par
3
+ * adresse (le site public d'Aplomb, P09-28), servi a la racine de son domaine par un hebergeur statique, sous une
4
+ * politique de securite du contenu STRICTE : ni `unsafe-inline`, ni `unsafe-hashes`.
5
+ *
6
+ * POURQUOI CETTE ETAPE EXISTE
7
+ *
8
+ * Le prerendu d'Angular (`outputMode: "static"`) ecrit les feuilles des composants en `<style ng-app-id>` dans la tete
9
+ * de chaque page, et les liaisons de style d'un composant (`st-logo` : sa hauteur, son rapport) en attributs `style`.
10
+ * Une politique stricte refuse les deux, et l'etape `securite` refuse les premieres : la page partirait sans les
11
+ * styles du socle. L'etape les sort de la page, apres `ng build` et avant la precompression :
12
+ *
13
+ * - les feuilles des composants de TOUTES les pages, dedoublonnees dans l'ordre de leur premiere apparition, dans
14
+ * UNE feuille hachee `composants-<empreinte>.css` (immuable, partagee par les pages), liee par chaque page la ou
15
+ * etait sa premiere feuille (l'ordre de la cascade ne change pas : apres les feuilles globales) ;
16
+ * - chaque attribut `style` en une regle de cette feuille, sur `data-st-style="<empreinte>"`, ses declarations en
17
+ * `!important` : la priorite d'un style en ligne, qu'aucune feuille de composant ne contredit ;
18
+ * - le prechargement de la police du premier ecran dans CHAQUE page, en adresse absolue (une page vit dans un
19
+ * sous-dossier : `media/` relatif viserait `/en/media/`).
20
+ *
21
+ * Le navigateur ne reprend pas les pages (ni hydratation ni rendu) : Angular y recreerait des `<style>` que la
22
+ * politique refuserait. Une page qui veut un comportement l'ecrit en amelioration progressive (`src/main.ts`).
23
+ */
24
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { empreinteCourte, fichiers, relatif } from '../commun.mjs';
27
+
28
+ /** Une feuille de composant ecrite par le prerendu, et le blanc qui la suit. */
29
+ const FEUILLE_DE_COMPOSANT = /<style ng-app-id="[^"]*">([\s\S]*?)<\/style>[ \t]*\n?/g;
30
+
31
+ /**
32
+ * Ce que le prerendu peut ecrire hors d'une balise et qu'il ne faut pas lire comme elle : un commentaire, le contenu
33
+ * d'un `script` ou d'un `style` ; puis une balise ouvrante, attributs entre guillemets compris (le prerendu n'ecrit
34
+ * jamais de guillemet nu dans une valeur : il l'echappe en `&quot;`).
35
+ */
36
+ const SEGMENT =
37
+ /<!--[\s\S]*?-->|<(script|style)\b[^>]*>[\s\S]*?<\/\1>|<[a-zA-Z][^\s/>]*(?:\s+[^\s"'=<>/]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?>/g;
38
+
39
+ /** L'attribut `style` d'une balise ouvrante. */
40
+ const ATTRIBUT_STYLE = /\sstyle="([^"]*)"/;
41
+
42
+ const CHARSET = /@charset\s+"[^"]*";\s*/gi;
43
+
44
+ /** La police du premier ecran, nommee par la recette `polices` du socle (voir `construire.mjs`). */
45
+ const POLICE = /^inter-latin-wght-normal-[A-Za-z0-9]+\.woff2$/;
46
+
47
+ /** Les pages du site : chaque `.html` du dossier livre, dans un ordre stable. */
48
+ export function pagesPrerendues(sortie) {
49
+ return fichiers(sortie).filter((chemin) => chemin.endsWith('.html'));
50
+ }
51
+
52
+ /** Les entites qu'une valeur d'attribut peut porter, rendues a leur caractere. */
53
+ function decoder(valeur) {
54
+ return valeur
55
+ .replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(Number.parseInt(code, 16)))
56
+ .replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number.parseInt(code, 10)))
57
+ .replace(/&quot;/g, '"')
58
+ .replace(/&apos;/g, "'")
59
+ .replace(/&lt;/g, '<')
60
+ .replace(/&gt;/g, '>')
61
+ .replace(/&nbsp;/g, ' ')
62
+ .replace(/&amp;/g, '&');
63
+ }
64
+
65
+ /** Les declarations d'un attribut `style`, chacune en `!important` ; un `;` entre parentheses ou guillemets reste. */
66
+ export function declarationsImportantes(style) {
67
+ const declarations = [];
68
+ let courante = '';
69
+ let profondeur = 0;
70
+ let guillemet = null;
71
+ for (const caractere of style) {
72
+ if (guillemet !== null) {
73
+ if (caractere === guillemet) guillemet = null;
74
+ } else if (caractere === '"' || caractere === "'") {
75
+ guillemet = caractere;
76
+ } else if (caractere === '(') {
77
+ profondeur += 1;
78
+ } else if (caractere === ')') {
79
+ profondeur = Math.max(0, profondeur - 1);
80
+ } else if (caractere === ';' && profondeur === 0) {
81
+ declarations.push(courante);
82
+ courante = '';
83
+ continue;
84
+ }
85
+ courante += caractere;
86
+ }
87
+ declarations.push(courante);
88
+ return declarations
89
+ .map((declaration) => declaration.trim())
90
+ .filter((declaration) => declaration.includes(':'))
91
+ .map((declaration) =>
92
+ /!\s*important$/i.test(declaration) ? declaration : `${declaration} !important`,
93
+ )
94
+ .join(';');
95
+ }
96
+
97
+ /**
98
+ * Sort les feuilles des composants et les attributs `style` de chaque page dans une feuille hachee. Rend
99
+ * `{ pages, feuilles, styles, feuille }` : les pages reecrites, les feuilles et les attributs distincts sortis, le nom
100
+ * de la feuille ecrite (`null` si rien n'etait a sortir : une seconde passe ne change rien).
101
+ */
102
+ export function externaliserLesStyles(sortie) {
103
+ const pages = pagesPrerendues(sortie).map((chemin) => ({
104
+ chemin,
105
+ contenu: readFileSync(chemin, 'utf8'),
106
+ }));
107
+ const feuilles = [];
108
+ const vues = new Set();
109
+ const regles = new Map();
110
+
111
+ for (const page of pages) {
112
+ for (const [, feuille] of page.contenu.matchAll(FEUILLE_DE_COMPOSANT)) {
113
+ const propre = feuille.replace(CHARSET, '').trim();
114
+ if (propre !== '' && !vues.has(propre)) {
115
+ vues.add(propre);
116
+ feuilles.push(propre);
117
+ }
118
+ }
119
+ page.contenu = page.contenu.replace(SEGMENT, (segment, rawText) => {
120
+ if (rawText !== undefined || segment.startsWith('<!--')) return segment;
121
+ const trouve = ATTRIBUT_STYLE.exec(segment);
122
+ if (trouve === null) return segment;
123
+ const declarations = declarationsImportantes(decoder(trouve[1]));
124
+ if (declarations === '') return segment.replace(ATTRIBUT_STYLE, '');
125
+ const empreinte = empreinteCourte(declarations).toLowerCase();
126
+ regles.set(empreinte, declarations);
127
+ return segment.replace(ATTRIBUT_STYLE, ` data-st-style="${empreinte}"`);
128
+ });
129
+ }
130
+
131
+ if (feuilles.length === 0 && regles.size === 0) {
132
+ return { pages: 0, feuilles: 0, styles: 0, feuille: null };
133
+ }
134
+ const contenu =
135
+ '@charset "UTF-8";\n' +
136
+ [
137
+ ...feuilles,
138
+ ...[...regles].map(
139
+ ([empreinte, declarations]) => `[data-st-style="${empreinte}"]{${declarations}}`,
140
+ ),
141
+ ].join('\n') +
142
+ '\n';
143
+ const feuille = `composants-${empreinteCourte(contenu)}.css`;
144
+ writeFileSync(join(sortie, feuille), contenu, 'utf8');
145
+ const lien = `<link rel="stylesheet" href="/${feuille}">`;
146
+
147
+ let reecrites = 0;
148
+ for (const page of pages) {
149
+ let premiere = true;
150
+ let contenuDeLaPage = page.contenu.replace(FEUILLE_DE_COMPOSANT, () => {
151
+ if (!premiere) return '';
152
+ premiere = false;
153
+ return lien;
154
+ });
155
+ // Une page sans feuille de composant, mais dont un attribut `style` est sorti, lie la feuille avant `</head>`.
156
+ if (
157
+ premiere &&
158
+ contenuDeLaPage.includes('data-st-style="') &&
159
+ contenuDeLaPage.includes('</head>')
160
+ ) {
161
+ contenuDeLaPage = contenuDeLaPage.replace('</head>', `${lien}</head>`);
162
+ }
163
+ if (contenuDeLaPage !== readFileSync(page.chemin, 'utf8')) {
164
+ writeFileSync(page.chemin, contenuDeLaPage, 'utf8');
165
+ reecrites += 1;
166
+ }
167
+ }
168
+ return { pages: reecrites, feuilles: feuilles.length, styles: regles.size, feuille };
169
+ }
170
+
171
+ /**
172
+ * Pose dans CHAQUE page le prechargement de la police du premier ecran, en adresse absolue (`/media/...`). Rend le nom
173
+ * de la police. La construction sans la police echoue : le premier rendu partirait dans la police du systeme.
174
+ */
175
+ export function prechargerLaPoliceDansLesPages(sortie) {
176
+ const media = join(sortie, 'media');
177
+ const polices = existsSync(media) ? readdirSync(media).filter((nom) => POLICE.test(nom)) : [];
178
+ if (polices.length !== 1) {
179
+ throw new Error(
180
+ `prechargement de la police : ${String(polices.length)} fichier(s) Inter latin dans media/ ` +
181
+ '(un attendu). La feuille des polices du socle est-elle dans angular.json ?',
182
+ );
183
+ }
184
+ const lien = `<link rel="preload" href="/media/${polices[0]}" as="font" type="font/woff2" crossorigin>`;
185
+ for (const chemin of pagesPrerendues(sortie)) {
186
+ const page = readFileSync(chemin, 'utf8');
187
+ if (page.includes(`href="/media/${polices[0]}"`)) continue;
188
+ if (!page.includes('</head>')) {
189
+ throw new Error(`prechargement de la police : ${relatif(sortie, chemin)} sans </head>.`);
190
+ }
191
+ writeFileSync(chemin, page.replace('</head>', ` ${lien}\n</head>`), 'utf8');
192
+ }
193
+ return polices[0];
194
+ }
195
+
196
+ /** Le resume d'une externalisation, pour le journal de la construction. */
197
+ export function resumerLExternalisation({ pages, feuilles, styles, feuille }) {
198
+ return feuille === null
199
+ ? 'aucune feuille de composant ni attribut style a sortir.'
200
+ : `${String(feuilles)} feuille(s) de composant et ${String(styles)} attribut(s) style sortis dans ` +
201
+ `${feuille}, ${String(pages)} page(s) reecrite(s).`;
202
+ }
@@ -22,7 +22,9 @@ const SPRITE_HACHE = /^sprite-[A-Z0-9]{8}\.svg$/;
22
22
 
23
23
  /**
24
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.
25
+ * comme l'hote : elle n'a ni remote ni sprite metier. Une application PRERENDUE (0.11.4) est servie a
26
+ * la racine de son domaine par un hebergeur statique, et non par le BFF sous `/ui/<nom>/` : son
27
+ * sprite est a `/assets/icones/`.
26
28
  */
27
29
  const duSocle = (projet) => projet.role === 'hote' || projet.role === 'autonome';
28
30
 
@@ -39,7 +41,10 @@ function reglages(projet) {
39
41
  'sprite.svg',
40
42
  ),
41
43
  dossier: join(projet.racine, 'public', 'assets', 'icones'),
42
- adresse: (fichier) => `/ui/${projet.nom}/assets/icones/${fichier}`,
44
+ adresse: (fichier) =>
45
+ projet.prerendu === true
46
+ ? `/assets/icones/${fichier}`
47
+ : `/ui/${projet.nom}/assets/icones/${fichier}`,
43
48
  genere: join(projet.racine, 'src', 'app', 'sprite-genere.ts'),
44
49
  constante: 'CHEMIN_DU_SPRITE',
45
50
  };
@@ -10,15 +10,15 @@
10
10
  * 3. LE POIDS DE SON ECRAN (`starterWeb.ecran` : `{ "entree": "src/app/pages/connexion.ts",
11
11
  * "seuilKo": 155 }`) : le chargement initial et la route de l'ecran, tout ce qu'ils importent
12
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).
13
+ * `starter-web construire` pose pour ce role), compares au seuil en octets exacts (0.11.5). Un seuil
14
+ * se baisse ; il ne se leve jamais sans mesure ecrite et datee (ADR 0031).
15
15
  *
16
16
  * Remplace `verification/application-autonome.mjs` que l'ecran anonyme d'identity tenait seul.
17
17
  */
18
18
  import { existsSync, readFileSync } from 'node:fs';
19
19
  import { dirname, join, resolve } from 'node:path';
20
20
  import { cloture, departDeLaPage, totaux } from '../cloture.mjs';
21
- import { fichiers, ko, lireJson, relatif } from '../commun.mjs';
21
+ import { depasse, fichiers, ko, lireJson, mesureEtSeuil, relatif } from '../commun.mjs';
22
22
 
23
23
  /** Ce qui trahit un moteur de federation dans un fichier livre. */
24
24
  const MOTEUR_DE_FEDERATION =
@@ -111,12 +111,13 @@ export default {
111
111
  } else {
112
112
  const { initial, ecran: poids } = poidsDeLEcran(projet.sortie, morceau);
113
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)`,
114
+ `chargement initial ${String(ko(initial.brotli))} Ko br (${String(initial.brotli)} o, ${String(initial.fichiers)} fichiers)`,
115
+ `ecran ${String(ecran.entree)} ${String(ko(poids.brotli))} Ko br (${String(poids.brotli)} o, ${String(poids.fichiers)} fichiers, ` +
116
+ `seuil ${String(ecran.seuilKo)} Ko br${typeof ecran.seuilKo === 'number' ? `, ${String(ecran.seuilKo * 1024)} o` : ''})`,
116
117
  );
117
- if (typeof ecran.seuilKo === 'number' && ko(poids.brotli) > ecran.seuilKo) {
118
+ if (typeof ecran.seuilKo === 'number' && depasse(poids.brotli, ecran.seuilKo)) {
118
119
  defauts.push(
119
- `ecran : ${String(ko(poids.brotli))} Ko br > ${String(ecran.seuilKo)} Ko br (un seuil ne se ` +
120
+ `ecran : ${mesureEtSeuil(poids.brotli, ecran.seuilKo)} (un seuil ne se ` +
120
121
  'leve jamais sans mesure ecrite et datee, ADR 0031).',
121
122
  );
122
123
  }
@@ -18,7 +18,7 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
18
18
  import { dirname, join } from 'node:path';
19
19
  import { brotliCompressSync, constants } from 'node:zlib';
20
20
  import { cloture, departDeLaPage, totaux } from '../cloture.mjs';
21
- import { fichiers, ko, lireJson, relatif } from '../commun.mjs';
21
+ import { depasse, fichiers, ko, lireJson, mesureEtSeuil, relatif } from '../commun.mjs';
22
22
  import { natureDesFichiers } from '../livraison.mjs';
23
23
  import { morceauDeLEntree } from './autonome.mjs';
24
24
 
@@ -48,6 +48,10 @@ export const BUDGETS_DU_REMOTE = { pageKo: 60, totalKo: 250, catalogueKo: 30 };
48
48
  * la coquille d'Aplomb avec le cablage du parcours, le contexte de compte, l'etat du bot (en `@defer`) et
49
49
  * le bouton d'arret permanent, 311,3 Ko.
50
50
  *
51
+ * Depuis la 0.11.5, chaque seuil se juge en OCTETS exacts (`depasse`) : arrondie au dixieme de Ko, une mesure
52
+ * qui le depassait de moins de 52 octets se lisait au seuil et passait. Mesure du 2026-10-11, coquille d'Aplomb
53
+ * sur la 0.11.4 : charge initiale 295 668 octets (seuil 295 936), premier ecran 319 479 (seuil 319 488).
54
+ *
51
55
  * ⚠️ Ce ne sont pas la cible du plan SPA §2.8 (170 Ko). L'ecart restant est STRUCTUREL : un paquet
52
56
  * partage en singleton part ENTIER, parce que n'importe quel remote peut en avoir besoin. Un seuil
53
57
  * se baisse ; il ne se leve jamais sans mesure ecrite et sans date.
@@ -137,17 +141,15 @@ function budgetDuRemote(projet) {
137
141
  details.push(
138
142
  `page ${nom} : ${String(ko(page.brotli))} Ko br (${String(page.fichiers)} fichiers)`,
139
143
  );
140
- if (ko(page.brotli) > BUDGETS_DU_REMOTE.pageKo) {
144
+ if (depasse(page.brotli, BUDGETS_DU_REMOTE.pageKo)) {
141
145
  defauts.push(
142
- `page ${nom} (${fichier}) : ${String(ko(page.brotli))} Ko br > ${String(BUDGETS_DU_REMOTE.pageKo)} Ko.`,
146
+ `page ${nom} (${fichier}) : ${mesureEtSeuil(page.brotli, BUDGETS_DU_REMOTE.pageKo)}.`,
143
147
  );
144
148
  }
145
149
  }
146
150
  const total = totaux(charges);
147
- if (ko(total.brotli) > BUDGETS_DU_REMOTE.totalKo) {
148
- defauts.push(
149
- `total charge : ${String(ko(total.brotli))} Ko br > ${String(BUDGETS_DU_REMOTE.totalKo)} Ko.`,
150
- );
151
+ if (depasse(total.brotli, BUDGETS_DU_REMOTE.totalKo)) {
152
+ defauts.push(`total charge : ${mesureEtSeuil(total.brotli, BUDGETS_DU_REMOTE.totalKo)}.`);
151
153
  }
152
154
  const dossierI18n = join(projet.sortie, 'i18n');
153
155
  const catalogues = existsSync(dossierI18n)
@@ -156,10 +158,8 @@ function budgetDuRemote(projet) {
156
158
  for (const catalogue of catalogues) {
157
159
  const taille = brotliDe(join(dossierI18n, catalogue));
158
160
  details.push(`i18n/${catalogue} : ${String(ko(taille))} Ko br`);
159
- if (ko(taille) > BUDGETS_DU_REMOTE.catalogueKo) {
160
- defauts.push(
161
- `i18n/${catalogue} : ${String(ko(taille))} Ko br > ${String(BUDGETS_DU_REMOTE.catalogueKo)} Ko.`,
162
- );
161
+ if (depasse(taille, BUDGETS_DU_REMOTE.catalogueKo)) {
162
+ defauts.push(`i18n/${catalogue} : ${mesureEtSeuil(taille, BUDGETS_DU_REMOTE.catalogueKo)}.`);
163
163
  }
164
164
  }
165
165
  return {
@@ -231,30 +231,31 @@ function budgetDeLHote(projet) {
231
231
  const premierEcran = premierEcranDeLHote(projet.sortie, depart, carte, morceaux);
232
232
  const ecran = totaux(premierEcran);
233
233
  const details = [
234
- `charge initiale ${String(ko(initiale.brotli))} Ko br (${String(initiale.fichiers)} fichiers)`,
235
- `premier ecran reel ${String(ko(ecran.brotli))} Ko br (${String(ecran.fichiers)} fichiers, ` +
234
+ `charge initiale ${String(ko(initiale.brotli))} Ko br (${String(initiale.brotli)} o, ` +
235
+ `${String(initiale.fichiers)} fichiers, seuil ${String(BUDGETS_DE_L_HOTE.chargeInitialeKo * 1024)} o)`,
236
+ `premier ecran reel ${String(ko(ecran.brotli))} Ko br (${String(ecran.brotli)} o, ` +
237
+ `${String(ecran.fichiers)} fichiers, seuil ${String(BUDGETS_DE_L_HOTE.premierEcranKo * 1024)} o, ` +
236
238
  `${morceaux === null ? 'toutes les pages de route' : 'entrees declarees'})`,
237
239
  ...[...premierEcran.entries()]
238
240
  .sort((a, b) => b[1].brotli - a[1].brotli)
239
241
  .slice(0, 5)
240
242
  .map(([chemin, taille]) => ` ${String(ko(taille.brotli))} Ko br ${chemin}`),
241
243
  ];
242
- if (ko(initiale.brotli) > BUDGETS_DE_L_HOTE.chargeInitialeKo) {
244
+ if (depasse(initiale.brotli, BUDGETS_DE_L_HOTE.chargeInitialeKo)) {
243
245
  defauts.push(
244
- `charge initiale : ${String(ko(initiale.brotli))} Ko br > ${String(BUDGETS_DE_L_HOTE.chargeInitialeKo)} Ko.`,
246
+ `charge initiale : ${mesureEtSeuil(initiale.brotli, BUDGETS_DE_L_HOTE.chargeInitialeKo)}.`,
245
247
  );
246
248
  }
247
- if (ko(ecran.brotli) > BUDGETS_DE_L_HOTE.premierEcranKo) {
249
+ if (depasse(ecran.brotli, BUDGETS_DE_L_HOTE.premierEcranKo)) {
248
250
  defauts.push(
249
- `premier ecran : ${String(ko(ecran.brotli))} Ko br > ${String(BUDGETS_DE_L_HOTE.premierEcranKo)} Ko (garde de non-regression).`,
251
+ `premier ecran : ${mesureEtSeuil(ecran.brotli, BUDGETS_DE_L_HOTE.premierEcranKo)} (garde de non-regression).`,
250
252
  );
251
253
  }
252
- const avertissements =
253
- ko(ecran.brotli) > BUDGETS_DE_L_HOTE.cibleDuPlanKo
254
- ? [
255
- `cible du plan SPA (section 2.8) : ${String(BUDGETS_DE_L_HOTE.cibleDuPlanKo)} Ko, ecart assume (ADR 0031).`,
256
- ]
257
- : [];
254
+ const avertissements = depasse(ecran.brotli, BUDGETS_DE_L_HOTE.cibleDuPlanKo)
255
+ ? [
256
+ `cible du plan SPA (section 2.8) : ${String(BUDGETS_DE_L_HOTE.cibleDuPlanKo)} Ko, ecart assume (ADR 0031).`,
257
+ ]
258
+ : [];
258
259
  return { defauts, avertissements, details, resume: 'premier ecran dans ses seuils.' };
259
260
  }
260
261
 
@@ -16,7 +16,8 @@
16
16
  * `<perimetre>.pages.<page>`, et la cle soeur `<perimetre>.aide.<page>` existe (le texte de
17
17
  * l'aide que `st-page-header` pose a cote du titre) ; une aide sans page est une faute de frappe ;
18
18
  * - l'hote porte les cles que `@starterlib/core` traduit par son catalogue
19
- * (`schemas/cles-exigees-du-socle.json`, constat A0837) ;
19
+ * (`schemas/cles-exigees-du-socle.json`, constat A0837), et l'application autonome qui pose le noyau
20
+ * comme lui ; une application prerendue, que le navigateur ne reprend pas, n'en porte aucune (0.11.4) ;
20
21
  * - le type des cles engendre (`src/app/i18n/cles.ts`, `starter-web i18n:types`) est a jour : une cle
21
22
  * inconnue dans `t()` est alors une erreur de compilation ;
22
23
  * - aucun texte ne porte une formulation que l'application interdit
@@ -258,6 +259,20 @@ export function jugerLesAides(catalogues, routes) {
258
259
  return defauts;
259
260
  }
260
261
 
262
+ /**
263
+ * Les cles que le noyau (`@starterlib/core` installe) traduit par le catalogue de l'application : celles que l'hote
264
+ * porte, et l'application autonome qui pose le noyau comme lui (constat A0837). Une application PRERENDUE
265
+ * (`starterWeb.prerendu`, 0.11.4) n'execute pas le noyau dans le navigateur, qui ne reprend pas ses pages : aucune.
266
+ */
267
+ export function clesExigeesDuNoyau(projet) {
268
+ if (projet.role !== 'hote' && projet.role !== 'autonome') return [];
269
+ if (projet.prerendu === true) return [];
270
+ const dossier = dossierInstalle(projet.racine, '@starterlib/core');
271
+ const exigees =
272
+ dossier === null ? null : lireJson(join(dossier, 'schemas', 'cles-exigees-du-socle.json'));
273
+ return exigees?.cles ?? [];
274
+ }
275
+
261
276
  function lire(chemin) {
262
277
  if (!existsSync(chemin)) return null;
263
278
  try {
@@ -351,17 +366,12 @@ export default {
351
366
 
352
367
  // L'hote, et l'application autonome qui pose le noyau comme lui, portent les cles que le noyau
353
368
  // traduit par son catalogue.
354
- if (projet.role === 'hote' || projet.role === 'autonome') {
355
- const dossier = dossierInstalle(projet.racine, '@starterlib/core');
356
- const exigees =
357
- dossier === null ? null : lireJson(join(dossier, 'schemas', 'cles-exigees-du-socle.json'));
358
- for (const cle of exigees?.cles ?? []) {
359
- for (const langue of LANGUES) {
360
- if (!Object.hasOwn(catalogues.socle?.[langue] ?? {}, cle)) {
361
- defauts.push(
362
- `socle.${langue}.json : cle "${cle}", exigee par @starterlib/core, absente.`,
363
- );
364
- }
369
+ for (const cle of clesExigeesDuNoyau(projet)) {
370
+ for (const langue of LANGUES) {
371
+ if (!Object.hasOwn(catalogues.socle?.[langue] ?? {}, cle)) {
372
+ defauts.push(
373
+ `socle.${langue}.json : cle "${cle}", exigee par @starterlib/core, absente.`,
374
+ );
365
375
  }
366
376
  }
367
377
  }
@@ -9,6 +9,12 @@
9
9
  *
10
10
  * Edge suit Chrome ; la WebView d'Android suit Chromium et WKWebView le Safari du systeme (ADR
11
11
  * 0043 du socle `api/starter`) : memes planchers.
12
+ *
13
+ * La WebView d'Android n'a pas de ligne a elle (0.11.5) : browserslist ne la connait qu'a sa derniere
14
+ * version (`Android >= 123` donne `android 152`), et la politique de prise en charge d'Angular (« baseline
15
+ * widely available ») ne la nomme pas ; chaque construction d'une SPA avertissait donc d'un navigateur
16
+ * « hors de la prise en charge d'Angular ». Elle suit Chromium : `Chrome` et `ChromeAndroid` au plancher
17
+ * de Chrome disent deja ce que la construction vise pour elle, et la cible produite ne change pas.
12
18
  */
13
19
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
20
  import { join } from 'node:path';
@@ -48,7 +54,6 @@ export function requetesDuPlancher({ chrome, safari, firefox }) {
48
54
  `Safari >= ${safari}`,
49
55
  `iOS >= ${safari}`,
50
56
  `ChromeAndroid >= ${chrome}`,
51
- `Android >= ${chrome}`,
52
57
  `FirefoxAndroid >= ${firefox}`,
53
58
  ];
54
59
  }
package/src/projet.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  * des qu'un `manifest.source.json` existe ;
11
11
  * - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
12
12
  * - le prefixe : `prefix` du projet d'`angular.json`, ou `starterWeb.prefixe` ;
13
+ * - le prerendu : `starterWeb.prerendu` (une application autonome servie en pages statiques, 0.11.4) ;
13
14
  * - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser`.
14
15
  *
15
16
  * Les options de la ligne de commande (`--nom`, `--prefixe`, `--pleine-page`) les remplacent : ce
@@ -68,6 +69,13 @@ export function lireLeProjet(racine, options = {}) {
68
69
  `starterWeb.role : "remote", "hote" ou "autonome", recu "${String(role)}".`,
69
70
  );
70
71
  }
72
+ // Une application prerendue (0.11.4) : un site statique, une page par adresse, que le navigateur ne reprend pas.
73
+ const prerendu = reglages.prerendu === true;
74
+ if (prerendu && role !== 'autonome') {
75
+ throw new ProjetIllisible(
76
+ `starterWeb.prerendu : reserve a une application autonome, pas a un role "${role}".`,
77
+ );
78
+ }
71
79
  const source = role === 'remote' ? lireJson(cheminSource) : null;
72
80
  if (role === 'remote' && source === null) {
73
81
  throw new ProjetIllisible('manifest.source.json absent ou illisible : un remote en a un.');
@@ -91,6 +99,7 @@ export function lireLeProjet(racine, options = {}) {
91
99
  nom,
92
100
  prefixe: options.prefixe ?? reglages.prefixe ?? projet.prefix ?? null,
93
101
  pleinePage: options.pleinePage ?? source?.pleinePage === true,
102
+ prerendu,
94
103
  source,
95
104
  nomDuProjetAngular: nomDuProjet,
96
105
  sortieRelative: sortie.split('\\').join('/'),