@starterlib/cli 0.10.2
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 +115 -0
- package/bin/starter-web.mjs +8 -0
- package/matrice-compatibilite.md +94 -0
- package/package.json +36 -0
- package/src/cloture.mjs +112 -0
- package/src/commun.mjs +61 -0
- package/src/construction/construire.mjs +72 -0
- package/src/construction/manifeste.mjs +159 -0
- package/src/construction/precompresser.mjs +127 -0
- package/src/construction/sprite.mjs +73 -0
- package/src/construction/types-i18n.mjs +60 -0
- package/src/etapes/budget.mjs +194 -0
- package/src/etapes/dependances.mjs +108 -0
- package/src/etapes/federation.mjs +94 -0
- package/src/etapes/hote.mjs +112 -0
- package/src/etapes/i18n.mjs +282 -0
- package/src/etapes/jetons.mjs +107 -0
- package/src/etapes/manifeste.mjs +120 -0
- package/src/etapes/outillage.mjs +116 -0
- package/src/etapes/securite.mjs +239 -0
- package/src/etapes/versions.mjs +184 -0
- package/src/index.mjs +12 -0
- package/src/lancer.mjs +211 -0
- package/src/livraison.mjs +45 -0
- package/src/matrice.mjs +119 -0
- package/src/projet.mjs +96 -0
- package/src/schema.mjs +188 -0
- package/src/socle-installe.mjs +52 -0
- package/src/trains.mjs +33 -0
- package/src/verifier.mjs +106 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @starterlib/cli
|
|
2
|
+
|
|
3
|
+
La chaine de verification et de construction d'une SPA du socle frontal : la coquille (l'hote) et
|
|
4
|
+
chaque remote. Elle remplace les huit outils que chaque SPA recopiait sous `tools/` (versions,
|
|
5
|
+
securite, budget, manifeste, sprite, empreinte, precompression, et ceux propres a la coquille) :
|
|
6
|
+
une correction du socle atteint desormais toutes les SPA a la montee de version, sans copie a
|
|
7
|
+
repercuter a la main.
|
|
8
|
+
|
|
9
|
+
Outil de Node (>= 24.15) : jamais en dependance de production. Aucune dependance a l'execution.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
A la version exacte des autres paquets du socle — ils forment un train et se montent ensemble :
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add -D -E @starterlib/cli@<version du socle>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Scripts d'une SPA :
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"start": "starter-web sprite && ng serve",
|
|
24
|
+
"build": "starter-web construire",
|
|
25
|
+
"build:dev": "starter-web construire --configuration development",
|
|
26
|
+
"test": "starter-web sprite && ng test",
|
|
27
|
+
"lint": "starter-web sprite && eslint .",
|
|
28
|
+
"verify": "starter-web verify"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commandes
|
|
33
|
+
|
|
34
|
+
| Commande | Ce qu'elle fait |
|
|
35
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
36
|
+
| `starter-web verify` | Les etapes ci-dessous, dans l'ordre, arretees a la premiere qui echoue, dont le nom est ecrit |
|
|
37
|
+
| `starter-web construire` | Sprite, `ng build`, prechargement de la police (hote), manifeste, precompression |
|
|
38
|
+
| `starter-web sprite` | Pose le sprite hache (`public/`) et sa constante engendree (`sprite-genere.ts`) |
|
|
39
|
+
| `starter-web manifeste` | Engendre `manifest.json` dans le dossier livre ; idempotent |
|
|
40
|
+
| `starter-web precompresser` | Ecrit les variantes `.br` et `.gz` ; `starter-web precompresser <dossier>` pour un autre dossier |
|
|
41
|
+
| `starter-web i18n:types` | Engendre `src/app/i18n/cles.ts`, le type des cles de chaque perimetre (`injecterTraduction<C>`) |
|
|
42
|
+
| `starter-web etapes` | Liste les etapes et les roles qu'elles concernent |
|
|
43
|
+
|
|
44
|
+
Codes de sortie : `0` conforme ; `1` une etape a echoue ; `2` la commande ne peut pas s'executer
|
|
45
|
+
(argument inconnu, projet illisible).
|
|
46
|
+
|
|
47
|
+
## Les etapes de `verify`
|
|
48
|
+
|
|
49
|
+
| Etape | Role | Ce qu'elle garde |
|
|
50
|
+
| ------------- | -------- | ----------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `versions` | les deux | Versions exactes de la matrice, greffon de federation lie a Angular, pnpm, Node, train du socle |
|
|
52
|
+
| `dependances` | les deux | Dependances de production revues, paquets interdits, aucun outil du socle recopie sous `tools/` |
|
|
53
|
+
| `jetons` | les deux | Chaque `var(--st-*)` sans repli existe dans `@starterlib/tokens` (feuilles et styles en ligne) |
|
|
54
|
+
| `format` | les deux | `prettier --check .` |
|
|
55
|
+
| `lint` | les deux | Prefixe des selecteurs (jamais `st-`), `eslint .`, puis stylelint (configuration du socle exigee) |
|
|
56
|
+
| `i18n` | les deux | Memes cles et arguments en fr et en, messages lisibles, cles appelees presentes, types a jour |
|
|
57
|
+
| `test` | les deux | `ng test --no-watch` |
|
|
58
|
+
| `contrat` | les deux | `pnpm run contrat:check` s'il existe (client d'API engendre, lot 175), sinon sautee en le disant |
|
|
59
|
+
| `build` | les deux | `starter-web construire` |
|
|
60
|
+
| `manifeste` | les deux | `validerLeManifesteDeRemote` et le schema de `@starterlib/core` INSTALLE ; le nom partout le meme |
|
|
61
|
+
| `federation` | les deux | Partage en singleton strict a plage explicite, Angular et le socle partages, aucun import nu orphelin |
|
|
62
|
+
| `entree` | hote | Aucun paquet partage importe statiquement avant `initFederation` |
|
|
63
|
+
| `budget` | les deux | En brotli. Remote : chaque page (60 Ko), le total (250 Ko), les catalogues. Hote : premier ecran |
|
|
64
|
+
| `amorcage` | hote | Aucun pair lourd (CDK, aria, formulaires, surfaces) dans le premier ecran |
|
|
65
|
+
| `securite` | les deux | Puits de l'ADR 0029 dans le code de la SPA, politiques Trusted Types exactes, cartes, SVG, meta CSP |
|
|
66
|
+
|
|
67
|
+
`--etapes a,b` n'en joue que certaines ; `--sauter a,b` en retire.
|
|
68
|
+
|
|
69
|
+
Ce que `securite` juge, et ou (lot 177). Le code du CADRE — ce que `remoteEntry.json` declare
|
|
70
|
+
partage : Angular, le socle, leurs morceaux internes — ecrit `innerHTML`, appelle `fetch` et pose
|
|
71
|
+
`:root` ; il n'est juge que sur les regles communes (eval, `new Function`, stockage, cookie, politiques
|
|
72
|
+
Trusted Types, cartes, SVG). Le code de la SPA l'est aussi sur `bypassSecurityTrust*`, les ecritures
|
|
73
|
+
HTML, `setAttribute('style')`, `Function(`, les minuteries sur une chaine et les ressources d'un autre
|
|
74
|
+
domaine ; un remote, en plus, sur `fetch` et `XMLHttpRequest`, `:root`, `!important` et les marqueurs
|
|
75
|
+
de l'hote de developpement (un remote ne demarre jamais seul en production : `src/main.ts` se remplace
|
|
76
|
+
par un point d'entree vide dans la configuration `production`).
|
|
77
|
+
|
|
78
|
+
## Ce que le CLI lit, et d'ou
|
|
79
|
+
|
|
80
|
+
- le role : `package.json#starterWeb.role` (`"hote"` pour la coquille), sinon `remote` des qu'un
|
|
81
|
+
`manifest.source.json` existe ;
|
|
82
|
+
- le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
|
|
83
|
+
- le prefixe des selecteurs : `prefix` du projet d'`angular.json` (ou `starterWeb.prefixe`) ;
|
|
84
|
+
- le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser` ;
|
|
85
|
+
- le train requis et le socle construit : `remoteEntry.json`, ce que la federation a reellement
|
|
86
|
+
embarque.
|
|
87
|
+
|
|
88
|
+
`--nom`, `--prefixe` et `--pleine-page` / `--pas-pleine-page` donnent les valeurs ATTENDUES : les
|
|
89
|
+
etapes signalent tout fichier qui s'en ecarte.
|
|
90
|
+
|
|
91
|
+
## La matrice et la regle de retard
|
|
92
|
+
|
|
93
|
+
Les versions viennent de `docs/matrice-compatibilite.md` du socle, publie avec le paquet et lu tel
|
|
94
|
+
quel : la matrice n'est ecrite qu'a un seul endroit.
|
|
95
|
+
|
|
96
|
+
Un remote peut avoir UN train de retard sur la coquille, pas davantage (ADR web 0006) : meme train,
|
|
97
|
+
vert ; un train de retard, `verify` avertit ; deux trains ou plus, `verify` echoue. Le dernier train
|
|
98
|
+
se lit au registre (`@starterlib/core`), ou se donne par `--train-de-reference <train>` (ou la
|
|
99
|
+
variable `STARTER_WEB_TRAIN_DE_REFERENCE`). Registre injoignable : le retard n'est pas mesure, et
|
|
100
|
+
`verify` le dit.
|
|
101
|
+
|
|
102
|
+
## Dependances admises
|
|
103
|
+
|
|
104
|
+
Une dependance de production hors de la liste revue (Angular, les paquets du socle servis a la page,
|
|
105
|
+
rxjs, tslib, es-module-shims) s'admet dans `package.json`, avec sa raison ecrite :
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"starterWeb": {
|
|
110
|
+
"dependancesAdmises": {
|
|
111
|
+
"date-fns": "Calcul d'echeances metier que le socle ne fournit pas ; revu le 2026-10-01."
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `starter-web` : la chaine de verification et de construction d'une SPA du socle frontal
|
|
4
|
+
* (@starterlib/cli, lot 174). Voir `src/lancer.mjs` et le README du paquet.
|
|
5
|
+
*/
|
|
6
|
+
import { lancer } from '../src/lancer.mjs';
|
|
7
|
+
|
|
8
|
+
process.exitCode = await lancer(process.argv.slice(2), { racine: process.cwd() });
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Matrice de compatibilite du socle frontal
|
|
2
|
+
|
|
3
|
+
Verifiee le 2026-09-26 contre le registre npm (lot 154). Toute montee de version se fait
|
|
4
|
+
**ligne par ligne, dans le meme commit**, et `pnpm versions:check` echoue si le
|
|
5
|
+
`package.json` s'en ecarte (version non exacte, autre que celle de la matrice, paquet Angular absent
|
|
6
|
+
de la matrice, greffon de federation d'une autre majeure.mineure qu'Angular, autre pnpm, Node sous le
|
|
7
|
+
plancher). Les depots de SPA portent la meme garde : `starter-web verify` (`@starterlib/cli`, lot 174)
|
|
8
|
+
lit CE document, publie avec lui, et ses essais le comparent a la matrice de `versions:check`.
|
|
9
|
+
|
|
10
|
+
⚠️ **Trois jours d'anciennete au moins** : `minimumReleaseAge: 4320` (`pnpm-workspace.yaml`) tient hors
|
|
11
|
+
du verrou toute version publiee depuis moins de trois jours (constat A1405). Une version plus recente,
|
|
12
|
+
meme corrective, attend : `@angular-architects/native-federation` 22.2.1 (publie le 2026-09-25) n'entre
|
|
13
|
+
pas au 2026-09-27 ; la 22.2.0 (2026-09-24) y entre. Angular monte en 22.2.0 le 2026-09-27 (demande du
|
|
14
|
+
proprietaire du 2026-09-26).
|
|
15
|
+
|
|
16
|
+
## Train `2026.1`
|
|
17
|
+
|
|
18
|
+
| Element | Version | Pourquoi cette version exactement |
|
|
19
|
+
| -------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
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
|
+
| `@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/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 |
|
|
23
|
+
| `ng-packagr` | `22.2.1` | Derniere stable de la ligne 22.2 (2026-09-23) |
|
|
24
|
+
| `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
|
+
| `node` | `>=24.15.0` | 24.x LTS, declare dans `engines` ; le meme plancher que la construction Maven des SPA (`frontend.node.version`) |
|
|
26
|
+
| `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`) |
|
|
27
|
+
| `vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` | `5.0.1` | Lanceur officiel du constructeur `@angular/build:unit-test` ; un meme numero pour toute la famille (des numeros meles chargent deux copies du lanceur) |
|
|
28
|
+
| `playwright`, `@playwright/test` | `1.63.0` | Le navigateur des essais : un correctif change un rendu mesure au pixel |
|
|
29
|
+
| `eslint` | `10.10.0` | `angular-eslint@22.5.0` accepte les majeurs 9 et 10 |
|
|
30
|
+
| `angular-eslint` | `22.5.0` | Suit le majeur d'Angular |
|
|
31
|
+
| `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
|
+
| `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
|
+
|
|
34
|
+
## A destination des depots de SPA (pas de ce depot)
|
|
35
|
+
|
|
36
|
+
| Element | Version | Avertissement |
|
|
37
|
+
| --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `@angular-architects/native-federation` | `22.2.0` | ⚠️ **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) |
|
|
39
|
+
| `orval` | `8.37.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 au 2026-09-25 ; a epingler a l'exact, en dependance de developpement |
|
|
40
|
+
| `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 |
|
|
41
|
+
| `@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 |
|
|
42
|
+
|
|
43
|
+
## Navigateurs
|
|
44
|
+
|
|
45
|
+
Plancher du socle (ADR web 0005) : **Chrome 123, Safari 17.5, Firefox 129** (Edge suit Chrome). C'est le
|
|
46
|
+
maximum, fonction par fonction, de ce que les feuilles et le noyau emploient sans repli :
|
|
47
|
+
|
|
48
|
+
| Fonction | Chrome | Safari | Firefox | Ou |
|
|
49
|
+
| ------------------------------------- | ------ | ------ | ------- | ---------------------------------------------------------- |
|
|
50
|
+
| `light-dark()` | 123 | 17.5 | 120 | feuille de jetons : une declaration par couleur (ADR 0009) |
|
|
51
|
+
| `:dir()` | 120 | 16.4 | 49 | feuilles des composants miroites en droite a gauche |
|
|
52
|
+
| `@starting-style` | 117 | 17.5 | 129 | apparition des surfaces et des blocs inseres |
|
|
53
|
+
| `transition-behavior: allow-discrete` | 117 | 17.4 | 129 | admis pour une sortie qui anime `display` |
|
|
54
|
+
|
|
55
|
+
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).
|
|
58
|
+
|
|
59
|
+
## Regle de retard
|
|
60
|
+
|
|
61
|
+
Un remote peut avoir **un train de retard** sur la coquille, pas davantage
|
|
62
|
+
(ADR web 0006, decision du proprietaire du 2026-09-25) : meme train, vert ; un
|
|
63
|
+
train de retard, la CI d'une SPA avertit (la coquille du nouveau train la retire
|
|
64
|
+
du menu tant qu'elle n'a pas monte) ; deux trains, elle echoue. Avant la 1.0, un
|
|
65
|
+
train est une mineure (`0.9.x`, puis `0.10.x`). Plus stricte que les deux trains
|
|
66
|
+
du socle backend : un remote retarde n'est plus servi, un service Java l'est
|
|
67
|
+
encore.
|
|
68
|
+
|
|
69
|
+
Une API depreciee en `0.N` ne disparait qu'a partir de `0.N+1`, et son
|
|
70
|
+
`@deprecated` dit son train de retrait (ADR web 0006). Avant la 1.0 : retrait
|
|
71
|
+
dans le train qui remplace, sans depreciation (avenant de l'ADR web 0006,
|
|
72
|
+
lot 176) ; la 0.9.0 ne garde donc rien de deprecie.
|
|
73
|
+
|
|
74
|
+
## Actions GitHub (verifie le 2026-09-16)
|
|
75
|
+
|
|
76
|
+
Epinglees au majeur, pas au commit : sans Renovate, un commit epingle ne
|
|
77
|
+
bougerait plus jamais, ce qui est pire qu'un majeur suivi. Le jour ou Renovate
|
|
78
|
+
arrive dans les depots de SPA, l'epinglage au commit devient tenable et se pose
|
|
79
|
+
partout d'un coup.
|
|
80
|
+
|
|
81
|
+
| Action | Version | Execution | Pourquoi |
|
|
82
|
+
| ------------------------- | ------- | --------- | ---------------------------------------------------------------------------- |
|
|
83
|
+
| `actions/checkout` | `v7` | Node 24 | Deux trains de retard atteints avec la v5 |
|
|
84
|
+
| `pnpm/action-setup` | `v6` | Node 24 | ⚠️ La v4 s'executait sur **Node 20**, deprecie par GitHub |
|
|
85
|
+
| `actions/setup-node` | `v7` | Node 24 | `cache: pnpm` reste explicite : la v6 n'a restreint que le cache automatique |
|
|
86
|
+
| `actions/upload-artifact` | `v7` | Node 24 | ⚠️ La v4 s'executait sur **Node 20**. La v6 exige un runner >= 2.327.1 |
|
|
87
|
+
|
|
88
|
+
## Ce que la matrice ne couvre pas encore
|
|
89
|
+
|
|
90
|
+
`transloco` entrera ici quand le premier depot de SPA l'emploiera ; il declare
|
|
91
|
+
une plage de pairs tres large (`>=16`), qui ne vaut pas compatibilite testee
|
|
92
|
+
avec la 22. `orval` y est depuis l'ADR web 0008, a la version relevee le
|
|
93
|
+
2026-09-25 : la compatibilite avec la 22 se prouvera au premier client engendre
|
|
94
|
+
(lot 175).
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@starterlib/cli",
|
|
3
|
+
"version": "0.10.2",
|
|
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
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"starter-web": "./bin/starter-web.mjs"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.mjs",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"matrice-compatibilite.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=24.15.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Arnoldkom/starter-web-platform.git",
|
|
28
|
+
"directory": "packages/cli"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Arnoldkom/starter-web-platform/tree/main/packages/cli#readme",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"starter",
|
|
33
|
+
"socle-frontal",
|
|
34
|
+
"cli"
|
|
35
|
+
]
|
|
36
|
+
}
|
package/src/cloture.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ce que le navigateur charge avant le premier ecran de l'hote, calcule une seule fois pour deux
|
|
3
|
+
* etapes : celle qui pese (`budget`) et celle qui interdit les aretes statiques vers un pair lourd
|
|
4
|
+
* (`amorcage`). Deux copies divergeraient au premier ajustement, et la garde finirait par surveiller
|
|
5
|
+
* autre chose que ce que la mesure mesure. (Venu de `tools/cloture.mjs` de la coquille, lot 174.)
|
|
6
|
+
*
|
|
7
|
+
* LA REGLE DE COUPE, qui est tout l'interet du module
|
|
8
|
+
*
|
|
9
|
+
* La frontiere entre « premier ecran » et « plus tard » ne suit PAS la frontiere
|
|
10
|
+
* statique/dynamique. Le point d'entree charge son amorcage par un import dynamique — il doit
|
|
11
|
+
* s'executer avant que la carte d'importation existe — et l'amorcage charge le gabarit et la page
|
|
12
|
+
* d'accueil de la meme facon. Ces deux niveaux sont attendus par la personne qui ouvre
|
|
13
|
+
* l'application. Les suivants sont des routes paresseuses et des remotes, qui ne le sont pas.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ Une seule exception, et elle se voit a l'oeil : un import DYNAMIQUE vers un PAQUET PARTAGE
|
|
16
|
+
* (specificateur nu, resolu par la carte d'importation) n'est jamais du premier ecran. C'est la
|
|
17
|
+
* forme qu'a prise le report du dialogue (ADR 0031) : l'hote ecrit
|
|
18
|
+
* `import('@starterlib/ui/surfaces')` pour que la CDK n'entre plus au demarrage. Le compter ici
|
|
19
|
+
* annulerait la mesure du gain. Seuls les morceaux LOCAUX sont donc suivis en dynamique.
|
|
20
|
+
*/
|
|
21
|
+
import { brotliCompressSync, constants, gzipSync } from 'node:zlib';
|
|
22
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
/** Les specificateurs importes statiquement par un module. */
|
|
26
|
+
function importsStatiques(source) {
|
|
27
|
+
const specificateurs = [];
|
|
28
|
+
for (const motif of [/\bfrom\s*["']([^"']+)["']/g, /\bimport\s*["']([^"']+)["']/g]) {
|
|
29
|
+
for (const [, specificateur] of source.matchAll(motif)) specificateurs.push(specificateur);
|
|
30
|
+
}
|
|
31
|
+
return specificateurs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Les specificateurs importes dynamiquement. */
|
|
35
|
+
function importsDynamiques(source) {
|
|
36
|
+
return [...source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)].map(
|
|
37
|
+
([, specificateur]) => specificateur,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Les fichiers nommes par la page elle-meme : scripts, feuilles, morceaux preordonnes. */
|
|
42
|
+
export function departDeLaPage(index) {
|
|
43
|
+
return [...index.matchAll(/(?:src|href)="([^"]+)"/g)]
|
|
44
|
+
.map(([, chemin]) => chemin)
|
|
45
|
+
.filter((chemin) => /\.(?:js|css)$/.test(chemin) && !/^(?:https?:)?\/\//.test(chemin));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Ce que le reseau transporte : la variante precompressee si elle existe, sinon le fichier. */
|
|
49
|
+
function transfere(sortie, chemin) {
|
|
50
|
+
const fichier = join(sortie, chemin);
|
|
51
|
+
const contenu = readFileSync(fichier);
|
|
52
|
+
const brotli = existsSync(`${fichier}.br`)
|
|
53
|
+
? statSync(`${fichier}.br`).size
|
|
54
|
+
: brotliCompressSync(contenu, {
|
|
55
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY },
|
|
56
|
+
}).length;
|
|
57
|
+
const gzip = existsSync(`${fichier}.gz`)
|
|
58
|
+
? statSync(`${fichier}.gz`).size
|
|
59
|
+
: gzipSync(contenu, { level: 9 }).length;
|
|
60
|
+
return { brotli, gzip, source: contenu.toString('utf8') };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* La cloture d'un ensemble de fichiers.
|
|
65
|
+
*
|
|
66
|
+
* @param sortie le dossier de la construction (`dist/<nom>/browser`)
|
|
67
|
+
* @param depart les fichiers de depart, relatifs a ce dossier
|
|
68
|
+
* @param carte la carte d'importation, pour resoudre les specificateurs nus
|
|
69
|
+
* @param profondeurDynamique 0 = aucun import dynamique suivi, 1 = ceux du depart, 2 = et ceux de
|
|
70
|
+
* l'amorcage
|
|
71
|
+
* @returns une Map du chemin vers ses tailles transferees et le fichier qui l'a fait entrer
|
|
72
|
+
*/
|
|
73
|
+
export function cloture(sortie, depart, carte, profondeurDynamique) {
|
|
74
|
+
const vus = new Set();
|
|
75
|
+
const poids = new Map();
|
|
76
|
+
const restants = depart.map((chemin) => ({ chemin, niveau: 0, parent: 'index.html' }));
|
|
77
|
+
while (restants.length > 0) {
|
|
78
|
+
const { chemin: brut, niveau, parent } = restants.shift();
|
|
79
|
+
const chemin = brut.replace(/^\.\//, '');
|
|
80
|
+
if (vus.has(chemin) || !existsSync(join(sortie, chemin))) continue;
|
|
81
|
+
vus.add(chemin);
|
|
82
|
+
const { brotli, gzip, source } = transfere(sortie, chemin);
|
|
83
|
+
poids.set(chemin, { brotli, gzip, parent });
|
|
84
|
+
if (!chemin.endsWith('.js')) continue;
|
|
85
|
+
const resoudre = (specificateur) =>
|
|
86
|
+
specificateur.startsWith('.')
|
|
87
|
+
? specificateur.replace(/^\.\//, '')
|
|
88
|
+
: (carte[specificateur] ?? null);
|
|
89
|
+
for (const specificateur of importsStatiques(source)) {
|
|
90
|
+
const resolu = resoudre(specificateur);
|
|
91
|
+
if (resolu !== null) restants.push({ chemin: resolu, niveau, parent: chemin });
|
|
92
|
+
}
|
|
93
|
+
if (niveau < profondeurDynamique) {
|
|
94
|
+
for (const specificateur of importsDynamiques(source)) {
|
|
95
|
+
// Voir la note de module : un import dynamique de paquet partage est du travail REPORTE.
|
|
96
|
+
if (!specificateur.startsWith('.')) continue;
|
|
97
|
+
const resolu = resoudre(specificateur);
|
|
98
|
+
if (resolu !== null) restants.push({ chemin: resolu, niveau: niveau + 1, parent: chemin });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return poids;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Les totaux transferes d'une cloture. */
|
|
106
|
+
export function totaux(poids) {
|
|
107
|
+
return {
|
|
108
|
+
brotli: [...poids.values()].reduce((somme, taille) => somme + taille.brotli, 0),
|
|
109
|
+
gzip: [...poids.values()].reduce((somme, taille) => somme + taille.gzip, 0),
|
|
110
|
+
fichiers: poids.size,
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/commun.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Les petits outils partages par les etapes : lecture de fichiers, tailles, empreintes, commandes.
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
|
+
import { join, relative, sep } from 'node:path';
|
|
8
|
+
|
|
9
|
+
/** Les fichiers d'un dossier, recursivement, dans un ordre stable ; `[]` s'il n'existe pas. */
|
|
10
|
+
export function fichiers(dossier) {
|
|
11
|
+
if (!existsSync(dossier)) return [];
|
|
12
|
+
const trouves = [];
|
|
13
|
+
for (const entree of readdirSync(dossier).sort()) {
|
|
14
|
+
const chemin = join(dossier, entree);
|
|
15
|
+
if (statSync(chemin).isDirectory()) trouves.push(...fichiers(chemin));
|
|
16
|
+
else trouves.push(chemin);
|
|
17
|
+
}
|
|
18
|
+
return trouves;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Le chemin de `chemin` depuis `base`, en barres obliques (le meme sous Windows et Linux). */
|
|
22
|
+
export function relatif(base, chemin) {
|
|
23
|
+
return relative(base, chemin).split(sep).join('/');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Un JSON lu, ou `null` s'il n'existe pas ou ne se lit pas. */
|
|
27
|
+
export function lireJson(chemin) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(chemin, 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Des octets en kilo-octets, au dixieme. */
|
|
36
|
+
export const ko = (octets) => Math.round((octets / 1024) * 10) / 10;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Empreinte courte d'un contenu, dans la forme que le socle Java reconnait comme « hachee » (huit
|
|
40
|
+
* caracteres alphanumeriques apres un tiret) : un actif ainsi nomme est servi immuable un an.
|
|
41
|
+
*/
|
|
42
|
+
export function empreinteCourte(contenu) {
|
|
43
|
+
return createHash('sha256').update(contenu).digest('hex').slice(0, 8).toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Lance une commande de l'outillage du projet (`pnpm exec eslint .`), sortie heritee, et rend son
|
|
48
|
+
* code. Sous Windows, `pnpm` est un script `.cmd` : il passe par l'interpreteur de commandes.
|
|
49
|
+
*/
|
|
50
|
+
export function executer(commande, arguments_, racine) {
|
|
51
|
+
const resultat = spawnSync(commande, arguments_, {
|
|
52
|
+
cwd: racine,
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
shell: process.platform === 'win32',
|
|
55
|
+
});
|
|
56
|
+
if (resultat.error !== undefined) {
|
|
57
|
+
console.error(` ${commande} ${arguments_.join(' ')} : ${resultat.error.message}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
return resultat.status ?? 1;
|
|
61
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `starter-web construire` : la construction d'une SPA, dans l'ordre qui a chacune sa raison.
|
|
3
|
+
*
|
|
4
|
+
* 1. le sprite hache et sa constante (le code l'importe) ;
|
|
5
|
+
* 2. `ng build` dans la configuration demandee ;
|
|
6
|
+
* 3. l'hote seul : le prechargement de la police du premier ecran ;
|
|
7
|
+
* 4. le manifeste (il renomme les catalogues i18n) ;
|
|
8
|
+
* 5. la precompression (apres le renommage, sans quoi des variantes resteraient orphelines).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { executer } from '../commun.mjs';
|
|
13
|
+
import { ecrireLeManifeste } from './manifeste.mjs';
|
|
14
|
+
import { juger, precompresser } from './precompresser.mjs';
|
|
15
|
+
import { poserLeSprite } from './sprite.mjs';
|
|
16
|
+
|
|
17
|
+
/** Inter latin, graisse variable, style normal : le nom que `polices.css` du socle donne. */
|
|
18
|
+
const POLICE = /^inter-latin-wght-normal-[A-Za-z0-9]+\.woff2$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pose dans `index.html` le prechargement de la SEULE police du premier ecran : Inter latin
|
|
22
|
+
* (recette `polices` du socle ; lot 239). La construction la recopie dans `media/` sous un nom a
|
|
23
|
+
* EMPREINTE : un lien ecrit a la main viserait un nom qui n'existe pas. L'etape lit le nom construit
|
|
24
|
+
* et pose le lien, avec `crossorigin` (une police se charge en mode CORS ; sans l'attribut, le
|
|
25
|
+
* prechargement est jete et la police demandee deux fois). Une construction sans le fichier echoue :
|
|
26
|
+
* le premier rendu partirait dans la police du systeme sans que rien ne le dise.
|
|
27
|
+
*/
|
|
28
|
+
export function prechargerLaPolice(sortie) {
|
|
29
|
+
const media = join(sortie, 'media');
|
|
30
|
+
const polices = existsSync(media) ? readdirSync(media).filter((nom) => POLICE.test(nom)) : [];
|
|
31
|
+
if (polices.length !== 1) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`prechargement de la police : ${String(polices.length)} fichier(s) Inter latin dans media/ ` +
|
|
34
|
+
'(un attendu). La feuille des polices du socle est-elle dans angular.json ?',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const chemin = join(sortie, 'index.html');
|
|
38
|
+
const page = readFileSync(chemin, 'utf8');
|
|
39
|
+
if (page.includes('rel="preload" href="media/inter-latin-wght-normal-')) return 'deja pose';
|
|
40
|
+
if (!page.includes('</head>'))
|
|
41
|
+
throw new Error('prechargement de la police : index.html sans </head>.');
|
|
42
|
+
const lien = `<link rel="preload" href="media/${polices[0]}" as="font" type="font/woff2" crossorigin>`;
|
|
43
|
+
writeFileSync(chemin, page.replace('</head>', ` ${lien}\n</head>`), 'utf8');
|
|
44
|
+
return polices[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Construit ; rend 0, ou le code de l'etape qui a echoue. `journal` : `{ info, erreur }`. */
|
|
48
|
+
export function construire(
|
|
49
|
+
projet,
|
|
50
|
+
{ configuration = 'production', journal = console, sprite = true } = {},
|
|
51
|
+
) {
|
|
52
|
+
if (sprite) journal.info(` sprite : ${poserLeSprite(projet)}`);
|
|
53
|
+
const code = executer(
|
|
54
|
+
'pnpm',
|
|
55
|
+
['exec', 'ng', 'build', '--configuration', configuration],
|
|
56
|
+
projet.racine,
|
|
57
|
+
);
|
|
58
|
+
if (code !== 0) {
|
|
59
|
+
journal.erreur(` ng build --configuration ${configuration} : code ${String(code)}.`);
|
|
60
|
+
return code;
|
|
61
|
+
}
|
|
62
|
+
if (projet.role === 'hote')
|
|
63
|
+
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
|
+
const { resume, defauts } = juger(precompresser(projet.sortie));
|
|
69
|
+
journal.info(` precompression : ${resume}`);
|
|
70
|
+
for (const defaut of defauts) journal.erreur(` - ${defaut}`);
|
|
71
|
+
return defauts.length === 0 ? 0 : 1;
|
|
72
|
+
}
|