@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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Etape `versions` : le `package.json` de la SPA contre la matrice du socle et le train de reference.
3
+ *
4
+ * Elle verifie :
5
+ * 1. tout paquet de la matrice, s'il est declare, l'est a sa version EXACTE et a celle de la
6
+ * matrice ; Angular, le greffon de federation et TypeScript sont exiges ;
7
+ * 2. tout paquet Angular installe figure a la matrice ;
8
+ * 3. le greffon de federation a la majeure.mineure d'Angular (235a : la matrice les liste, cette
9
+ * regle les LIE) ;
10
+ * 4. zone.js n'est installe nulle part (le socle est zoneless) ;
11
+ * 5. pnpm et le plancher de Node sont ceux de la matrice ;
12
+ * 6. les paquets du socle, CLI compris, sont a la MEME version exacte, celle de ce CLI : ils
13
+ * forment un train et se montent ensemble ;
14
+ * 7. le train de la SPA et le train de reference (le dernier publie) : meme train, vert ; un train
15
+ * de retard, un avertissement (la coquille du nouveau train retire la SPA de son menu tant
16
+ * qu'elle n'a pas monte) ; deux trains ou plus, un echec (docs/matrice-compatibilite.md,
17
+ * « Regle de retard », ADR web 0006).
18
+ */
19
+ import { spawnSync } from 'node:child_process';
20
+ import { matriceDuSocle, versionDuSocle } from '../matrice.mjs';
21
+ import { retardEnTrains, trainDe } from '../trains.mjs';
22
+
23
+ const FEDERATION = '@angular-architects/native-federation';
24
+
25
+ /** Exiges dans toute SPA : ce que la matrice epingle et sans quoi rien ne se construit. */
26
+ const EXIGES = [
27
+ '@angular/core',
28
+ '@angular/common',
29
+ '@angular/compiler',
30
+ '@angular/compiler-cli',
31
+ '@angular/forms',
32
+ '@angular/platform-browser',
33
+ '@angular/router',
34
+ '@angular/build',
35
+ '@angular/cli',
36
+ FEDERATION,
37
+ 'es-module-shims',
38
+ 'typescript',
39
+ '@starterlib/core',
40
+ '@starterlib/tokens',
41
+ '@starterlib/ui',
42
+ '@starterlib/util',
43
+ ];
44
+
45
+ /** `22.1.7` donne `22.1` ; `null` pour ce qui n'est pas un numero. */
46
+ function majeureMineure(version) {
47
+ const trouve = /^(\d+)\.(\d+)\./.exec(version);
48
+ return trouve === null ? null : `${trouve[1]}.${trouve[2]}`;
49
+ }
50
+
51
+ /**
52
+ * Le train de reference : l'option `--train-de-reference`, la variable
53
+ * `STARTER_WEB_TRAIN_DE_REFERENCE`, sinon le dernier `@starterlib/core` publie au registre.
54
+ * `null` si rien ne le donne (registre injoignable) : le retard n'est alors pas mesure, et le dire.
55
+ */
56
+ function trainDeReference(contexte) {
57
+ const donne = contexte.trainDeReference ?? process.env.STARTER_WEB_TRAIN_DE_REFERENCE;
58
+ if (donne !== undefined && donne !== '') return { train: trainDe(donne), origine: 'donne' };
59
+ const resultat = spawnSync('npm', ['view', '@starterlib/core', 'version', '--json'], {
60
+ encoding: 'utf8',
61
+ timeout: 20_000,
62
+ shell: process.platform === 'win32',
63
+ });
64
+ if (resultat.status !== 0) return { train: null, origine: 'registre injoignable' };
65
+ try {
66
+ return { train: trainDe(JSON.parse(resultat.stdout)), origine: 'registre' };
67
+ } catch {
68
+ return { train: null, origine: 'reponse du registre illisible' };
69
+ }
70
+ }
71
+
72
+ /** Les fautes et avertissements de `paquet` (le package.json) ; exporte pour les essais. */
73
+ export function jugerLesVersions(paquet, { matrice, socle, reference }) {
74
+ const declarees = { ...paquet.dependencies, ...paquet.devDependencies };
75
+ const defauts = [];
76
+ const avertissements = [];
77
+
78
+ for (const nom of EXIGES) {
79
+ if (declarees[nom] === undefined) defauts.push(`${nom} : absent de package.json.`);
80
+ }
81
+
82
+ for (const [nom, attendue] of Object.entries(matrice.paquets)) {
83
+ const declaree = declarees[nom];
84
+ if (declaree === undefined) continue;
85
+ if (/^[\^~><=*]/.test(declaree)) {
86
+ defauts.push(
87
+ `${nom} : plage "${declaree}", version exacte obligatoire (${attendue}, matrice). Une plage ` +
88
+ 'laisse la CI et le poste diverger.',
89
+ );
90
+ } else if (declaree !== attendue) {
91
+ defauts.push(`${nom} : ${declaree}, attendu ${attendue} (exact, matrice).`);
92
+ }
93
+ }
94
+
95
+ for (const [nom, version] of Object.entries(declarees)) {
96
+ if (nom.startsWith('@angular/') && !Object.hasOwn(matrice.paquets, nom)) {
97
+ defauts.push(`${nom} (${version}) est installe mais absent de la matrice de compatibilite.`);
98
+ }
99
+ }
100
+
101
+ const angular = declarees['@angular/core'];
102
+ const federation = declarees[FEDERATION];
103
+ if (
104
+ angular !== undefined &&
105
+ federation !== undefined &&
106
+ majeureMineure(angular) !== majeureMineure(federation)
107
+ ) {
108
+ defauts.push(
109
+ `${FEDERATION} ${federation} : la majeure.mineure du greffon doit etre celle d'@angular/core ` +
110
+ `(${angular}) ; les monter ensemble, dans le meme commit (235a).`,
111
+ );
112
+ }
113
+
114
+ if (Object.hasOwn(declarees, 'zone.js')) {
115
+ defauts.push('zone.js : interdit, le socle est zoneless.');
116
+ }
117
+ if (paquet.packageManager !== `pnpm@${matrice.pnpm}`) {
118
+ defauts.push(
119
+ `packageManager : ${String(paquet.packageManager)}, attendu pnpm@${matrice.pnpm}.`,
120
+ );
121
+ }
122
+ const node = paquet.engines?.node ?? '';
123
+ if (!node.startsWith(`>=${matrice.nodeMinimal}`)) {
124
+ defauts.push(
125
+ `engines.node "${node}" : le plancher de la matrice est >=${matrice.nodeMinimal}.`,
126
+ );
127
+ }
128
+
129
+ // Les paquets du socle forment un train : une version, la meme partout, celle de ce CLI.
130
+ const duSocle = Object.entries(declarees).filter(([nom]) => nom.startsWith('@starterlib/'));
131
+ for (const [nom, version] of duSocle) {
132
+ if (/^[\^~><=*]/.test(version)) {
133
+ defauts.push(`${nom} : plage "${version}", version exacte obligatoire (${socle}).`);
134
+ } else if (version !== socle) {
135
+ defauts.push(
136
+ `${nom} : ${version}, attendu ${socle} (la version de @starterlib/cli : les paquets du ` +
137
+ 'socle se montent ensemble).',
138
+ );
139
+ }
140
+ }
141
+
142
+ const train = trainDe(socle);
143
+ if (reference.train === null) {
144
+ avertissements.push(
145
+ `train de reference inconnu (${reference.origine}) : le retard sur le dernier train n'est pas ` +
146
+ 'mesure. --train-de-reference <train> le fixe.',
147
+ );
148
+ } else {
149
+ const retard = retardEnTrains(train, reference.train);
150
+ if (retard === 1) {
151
+ avertissements.push(
152
+ `un train de retard (${train}, dernier train ${reference.train}) : la coquille du nouveau ` +
153
+ 'train retire cette SPA de son menu tant qu elle n a pas monte.',
154
+ );
155
+ } else if (retard >= 2) {
156
+ defauts.push(
157
+ `${String(retard)} trains de retard (${train}, dernier train ${reference.train}) : un remote ` +
158
+ 'peut avoir un train de retard sur la coquille, pas davantage (ADR web 0006).',
159
+ );
160
+ }
161
+ }
162
+
163
+ return { defauts, avertissements, train };
164
+ }
165
+
166
+ export default {
167
+ nom: 'versions',
168
+ roles: ['remote', 'hote'],
169
+ executer(projet, contexte) {
170
+ const matrice = matriceDuSocle();
171
+ const socle = versionDuSocle();
172
+ const reference = trainDeReference(contexte);
173
+ const { defauts, avertissements, train } = jugerLesVersions(projet.paquet, {
174
+ matrice,
175
+ socle,
176
+ reference,
177
+ });
178
+ return {
179
+ defauts,
180
+ avertissements,
181
+ resume: `conformes a la matrice (socle ${socle}, train ${String(train)}, Angular ${matrice.paquets['@angular/core']}).`,
182
+ };
183
+ },
184
+ };
package/src/index.mjs ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * L'API programmatique de `@starterlib/cli`, pour un outil qui l'appelle sans passer par la ligne
3
+ * de commande (un script de fixtures, un essai). La ligne de commande est `starter-web`.
4
+ */
5
+ export { lancer, lireLesArguments } from './lancer.mjs';
6
+ export { lireLeProjet, ProjetIllisible } from './projet.mjs';
7
+ export { ETAPES, verifier } from './verifier.mjs';
8
+ export { lireLaMatrice, matriceDuSocle, versionDuSocle } from './matrice.mjs';
9
+ export { comparerLesTrains, retardEnTrains, trainDe } from './trains.mjs';
10
+ export { precompresser } from './construction/precompresser.mjs';
11
+ export { ecrireLeManifeste } from './construction/manifeste.mjs';
12
+ export { poserLeSprite } from './construction/sprite.mjs';
package/src/lancer.mjs ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * La ligne de commande `starter-web` (lot 174).
3
+ *
4
+ * Codes de sortie : 0 conforme ; 1 une etape a echoue, et son nom est ecrit ; 2 la commande ne peut
5
+ * pas s'executer (argument inconnu, projet illisible).
6
+ */
7
+ import { construire } from './construction/construire.mjs';
8
+ import { ecrireLeManifeste } from './construction/manifeste.mjs';
9
+ import { juger, precompresser } from './construction/precompresser.mjs';
10
+ import { poserLeSprite } from './construction/sprite.mjs';
11
+ import { ecrireLesTypesDeCles } from './construction/types-i18n.mjs';
12
+ import { lireJson, relatif } from './commun.mjs';
13
+ import { versionDuSocle } from './matrice.mjs';
14
+ import { ProjetIllisible, lireLeProjet } from './projet.mjs';
15
+ import { ETAPES, etapesInconnues, verifier } from './verifier.mjs';
16
+ import { join, resolve } from 'node:path';
17
+
18
+ const AIDE = `starter-web : la chaine d'une SPA du socle frontal (@starterlib/cli).
19
+
20
+ Commandes :
21
+ verify les etapes, dans l'ordre, arretees a la premiere qui echoue
22
+ construire sprite, ng build, manifeste, precompression (--configuration development)
23
+ sprite pose le sprite hache et sa constante
24
+ manifeste engendre manifest.json dans le dossier livre
25
+ precompresser ecrit les variantes .br et .gz (dossier livre, ou le dossier donne)
26
+ i18n:types engendre le type des cles de chaque perimetre (src/app/i18n/cles.ts)
27
+ etapes liste les etapes de verify
28
+
29
+ Options de verify :
30
+ --etapes a,b seulement ces etapes
31
+ --sauter a,b toutes sauf celles-ci
32
+ --train-de-reference T le dernier train publie (sinon lu au registre)
33
+
34
+ Options du projet (valeurs attendues, lues sinon dans ses fichiers) :
35
+ --nom <nom> --prefixe <prefixe> --pleine-page | --pas-pleine-page
36
+ `;
37
+
38
+ /** Les arguments en `{ commande, positionnels, options }` ; leve sur une option inconnue. */
39
+ export function lireLesArguments(argv) {
40
+ const options = {};
41
+ const positionnels = [];
42
+ const valeur = (indice, nom) => {
43
+ const suivant = argv[indice + 1];
44
+ if (suivant === undefined || suivant.startsWith('--'))
45
+ throw new Error(`${nom} attend une valeur.`);
46
+ return suivant;
47
+ };
48
+ const liste = (texte) =>
49
+ texte
50
+ .split(',')
51
+ .map((nom) => nom.trim())
52
+ .filter((nom) => nom !== '');
53
+ for (let indice = 0; indice < argv.length; indice += 1) {
54
+ const argument = argv[indice];
55
+ switch (argument) {
56
+ case '--etapes':
57
+ options.etapes = liste(valeur(indice, argument));
58
+ indice += 1;
59
+ break;
60
+ case '--sauter':
61
+ options.sauter = liste(valeur(indice, argument));
62
+ indice += 1;
63
+ break;
64
+ case '--train-de-reference':
65
+ options.trainDeReference = valeur(indice, argument);
66
+ indice += 1;
67
+ break;
68
+ case '--nom':
69
+ options.nom = valeur(indice, argument);
70
+ indice += 1;
71
+ break;
72
+ case '--prefixe':
73
+ options.prefixe = valeur(indice, argument);
74
+ indice += 1;
75
+ break;
76
+ case '--configuration':
77
+ options.configuration = valeur(indice, argument);
78
+ indice += 1;
79
+ break;
80
+ case '--pleine-page':
81
+ options.pleinePage = true;
82
+ break;
83
+ case '--pas-pleine-page':
84
+ options.pleinePage = false;
85
+ break;
86
+ case '--aide':
87
+ case '-h':
88
+ options.aide = true;
89
+ break;
90
+ case '--version':
91
+ case '-v':
92
+ options.version = true;
93
+ break;
94
+ default:
95
+ if (argument.startsWith('-')) throw new Error(`option inconnue : ${argument}`);
96
+ positionnels.push(argument);
97
+ }
98
+ }
99
+ const [commande, ...reste] = positionnels;
100
+ return { commande, positionnels: reste, options };
101
+ }
102
+
103
+ function precompresserEtDire(dossier, sortie) {
104
+ const { resume, defauts } = juger(precompresser(dossier));
105
+ sortie.info(`precompression : ${resume}`);
106
+ for (const defaut of defauts) sortie.erreur(` - ${defaut}`);
107
+ return defauts.length === 0 ? 0 : 1;
108
+ }
109
+
110
+ /**
111
+ * Lance `argv` (sans `node` ni le script) depuis `racine`. `journal` : `{ info, erreur }`. Rend le
112
+ * code de sortie.
113
+ */
114
+ export async function lancer(argv, { racine = process.cwd(), journal } = {}) {
115
+ const sortie = journal ?? {
116
+ info: (texte) => console.log(texte),
117
+ erreur: (texte) => console.error(texte),
118
+ };
119
+ let lu;
120
+ try {
121
+ lu = lireLesArguments(argv);
122
+ } catch (erreur) {
123
+ sortie.erreur(`starter-web : ${erreur.message}\n\n${AIDE}`);
124
+ return 2;
125
+ }
126
+ const { commande, positionnels, options } = lu;
127
+ if (options.version) {
128
+ sortie.info(versionDuSocle());
129
+ return 0;
130
+ }
131
+ if (options.aide || commande === undefined || commande === 'aide') {
132
+ sortie.info(AIDE);
133
+ return commande === undefined && !options.aide ? 2 : 0;
134
+ }
135
+ if (commande === 'etapes') {
136
+ for (const etape of ETAPES) sortie.info(`${etape.nom.padEnd(12)} ${etape.roles.join(', ')}`);
137
+ return 0;
138
+ }
139
+
140
+ const inconnues = etapesInconnues([...(options.etapes ?? []), ...(options.sauter ?? [])]);
141
+ if (inconnues.length > 0) {
142
+ sortie.erreur(
143
+ `starter-web : etape(s) inconnue(s) : ${inconnues.join(', ')} (starter-web etapes).`,
144
+ );
145
+ return 2;
146
+ }
147
+
148
+ // Un dossier donne se precompresse sans projet autour : une fixture reecrite, un dossier d'essai.
149
+ if (commande === 'precompresser' && positionnels[0] !== undefined) {
150
+ return precompresserEtDire(resolve(racine, positionnels[0]), sortie);
151
+ }
152
+
153
+ let projet;
154
+ try {
155
+ projet = lireLeProjet(racine, options);
156
+ } catch (erreur) {
157
+ if (erreur instanceof ProjetIllisible) {
158
+ sortie.erreur(`starter-web : ${erreur.message}`);
159
+ return 2;
160
+ }
161
+ throw erreur;
162
+ }
163
+
164
+ try {
165
+ switch (commande) {
166
+ case 'verify': {
167
+ sortie.info(
168
+ `starter-web verify ${versionDuSocle()} : ${projet.nom} (${projet.role}, prefixe ` +
169
+ `${String(projet.prefixe)}, pleine page ${projet.pleinePage ? 'oui' : 'non'}, ` +
170
+ `${projet.sortieRelative})`,
171
+ );
172
+ return await verifier(projet, { ...options, journal: sortie });
173
+ }
174
+ case 'construire':
175
+ return construire(projet, { configuration: options.configuration, journal: sortie });
176
+ case 'sprite':
177
+ sortie.info(`sprite : ${poserLeSprite(projet)}`);
178
+ return 0;
179
+ case 'manifeste': {
180
+ const manifeste = ecrireLeManifeste(projet);
181
+ sortie.info(
182
+ `manifest.json : ${manifeste.nom} ${manifeste.version}, socle ${manifeste.socle.construit}, Angular ${manifeste.angular}`,
183
+ );
184
+ return 0;
185
+ }
186
+ case 'precompresser':
187
+ return precompresserEtDire(projet.sortie, sortie);
188
+ case 'i18n:types': {
189
+ const perimetres =
190
+ projet.role === 'remote' ? (projet.source?.i18n?.perimetres ?? []) : ['socle'];
191
+ const catalogues = Object.fromEntries(
192
+ perimetres.map((perimetre) => [
193
+ perimetre,
194
+ { fr: lireJson(join(projet.racine, 'public', 'i18n', `${perimetre}.fr.json`)) },
195
+ ]),
196
+ );
197
+ const chemin = ecrireLesTypesDeCles(projet.racine, perimetres, catalogues);
198
+ sortie.info(`i18n:types : ${relatif(projet.racine, chemin)} (${perimetres.join(', ')})`);
199
+ return 0;
200
+ }
201
+ default:
202
+ sortie.erreur(`starter-web : commande inconnue : ${commande}\n\n${AIDE}`);
203
+ return 2;
204
+ }
205
+ } catch (erreur) {
206
+ sortie.erreur(
207
+ `starter-web ${commande} : ${erreur instanceof Error ? erreur.message : String(erreur)}`,
208
+ );
209
+ return 1;
210
+ }
211
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * La nature des fichiers d'un livrable federe, lue dans `remoteEntry.json` (lot 177).
3
+ *
4
+ * - PARTAGES : les paquets que la federation partage (`shared[].outFileName`) et leurs morceaux
5
+ * internes (`chunks['browser-shared']`, adresses `@nf-internal/...` de la carte d'importation) :
6
+ * le code d'Angular et du socle, charge une fois par la coquille pour toutes les SPA.
7
+ * - EXPOSES : ce que la coquille charge du remote (`exposes[].outFileName` et
8
+ * `chunks['mapping-or-exposed']`) : ses routes, ses pages et leurs morceaux.
9
+ * - le RESTE : l'application autonome (`main`, ses morceaux, `index.html`), que la coquille ne
10
+ * charge jamais d'un remote.
11
+ *
12
+ * Les gardes de l'ADR 0029 jugent le code de la SPA, pas celui du cadre : Angular ecrit
13
+ * `innerHTML` et appelle `fetch`, le socle aussi (televersement direct), et la garde de securite du
14
+ * socle juge deja ses propres paquets construits.
15
+ */
16
+
17
+ /** `{ partages, exposes }` : deux ensembles de chemins relatifs au dossier livre. */
18
+ export function natureDesFichiers(entree) {
19
+ return {
20
+ partages: new Set([
21
+ ...(entree?.shared ?? []).map((partage) => partage.outFileName),
22
+ ...(entree?.chunks?.['browser-shared'] ?? []),
23
+ ]),
24
+ exposes: new Set([
25
+ ...(entree?.exposes ?? []).map((expose) => expose.outFileName),
26
+ ...(entree?.chunks?.['mapping-or-exposed'] ?? []),
27
+ ]),
28
+ };
29
+ }
30
+
31
+ /** Un specificateur nu, tel qu'un module ES construit l'ecrit (`@angular/core`, `rxjs/operators`). */
32
+ export const SPECIFICATEUR_NU = /^@?[a-z0-9][\w.-]*(?:\/[\w.@-]+)*$/i;
33
+
34
+ /** Les specificateurs importes par un module construit, statiquement et dynamiquement. */
35
+ export function specificateurs(code) {
36
+ const trouves = [];
37
+ for (const motif of [
38
+ /\bfrom\s*["']([^"']+)["']/g,
39
+ /\bimport\s*["']([^"']+)["']/g,
40
+ /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
41
+ ]) {
42
+ for (const [, specificateur] of code.matchAll(motif)) trouves.push(specificateur);
43
+ }
44
+ return trouves;
45
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * La matrice de compatibilite, lue dans le DOCUMENT ou le proprietaire l'ecrit (lot 174).
3
+ *
4
+ * `docs/matrice-compatibilite.md` est la source : chaque montee s'y ecrit ligne par ligne, avec sa
5
+ * raison. Les copies recopiees dans chaque SPA (`tools/verifier-versions.mjs`) divergeaient des le
6
+ * premier ajustement ; le CLI lit le document publie avec lui, et une garde du socle compare cette
7
+ * lecture a la matrice de sa propre garde (`test/matrice.test.mjs`).
8
+ *
9
+ * Ce qui est lu : les tableaux des sections « Train » et « A destination des depots de SPA ». Dans la
10
+ * colonne des elements, un nom sans portee qui suit un nom porte (`@angular/core`, `common`) herite
11
+ * de sa portee ; une cellule de versions a une seule valeur s'applique a toute la ligne, sinon les
12
+ * versions s'appairent aux noms dans l'ordre. `node` donne le plancher, `pnpm` le gestionnaire.
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { dirname, join } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const ICI = dirname(fileURLToPath(import.meta.url));
19
+
20
+ /** Les sections dont les tableaux font la matrice. */
21
+ const SECTIONS = [/^## Train\b/, /^## A destination des depots de SPA\b/];
22
+
23
+ /** Le document : publie a la racine du paquet, ou lu dans le depot du socle. */
24
+ export function cheminDeLaMatrice() {
25
+ const candidats = [
26
+ join(ICI, '..', 'matrice-compatibilite.md'),
27
+ join(ICI, '..', '..', '..', 'docs', 'matrice-compatibilite.md'),
28
+ ];
29
+ const trouve = candidats.find((candidat) => existsSync(candidat));
30
+ if (trouve === undefined) {
31
+ throw new Error(
32
+ `matrice-compatibilite.md introuvable (${candidats.join(', ')}) : paquet incomplet.`,
33
+ );
34
+ }
35
+ return trouve;
36
+ }
37
+
38
+ /** Les cellules des lignes de donnees d'un tableau markdown (en-tete et separateur exclus). */
39
+ function lignesDeTableau(lignes) {
40
+ const tableau = lignes.filter((ligne) => ligne.trimStart().startsWith('|'));
41
+ return tableau
42
+ .slice(2)
43
+ .map((ligne) => ligne.trim().replace(/^\|/, '').replace(/\|$/, '').split('|'))
44
+ .map((cellules) => cellules.map((cellule) => cellule.trim()));
45
+ }
46
+
47
+ /** Les noms entre accents graves d'une cellule, portee heritee (`@angular/core`, `common`). */
48
+ function nomsDeLaCellule(cellule) {
49
+ const noms = [];
50
+ let portee = null;
51
+ for (const [, brut] of cellule.matchAll(/`([^`]+)`/g)) {
52
+ if (brut.startsWith('@')) {
53
+ portee = brut.split('/')[0];
54
+ noms.push(brut);
55
+ } else if (portee !== null && !brut.includes('/')) {
56
+ noms.push(`${portee}/${brut}`);
57
+ } else {
58
+ noms.push(brut);
59
+ }
60
+ }
61
+ return noms;
62
+ }
63
+
64
+ /**
65
+ * La matrice ecrite dans `texte` : `{ train, paquets, pnpm, nodeMinimal }`. Leve si le document ne
66
+ * porte plus ce que le CLI sait lire : une matrice lue a moitie laisserait passer en silence ce
67
+ * qu'elle ne voit plus.
68
+ */
69
+ export function lireLaMatrice(texte) {
70
+ const lignes = texte.split(/\r?\n/);
71
+ const paquets = {};
72
+ let pnpm = null;
73
+ let nodeMinimal = null;
74
+ let train = null;
75
+
76
+ for (const section of SECTIONS) {
77
+ const debut = lignes.findIndex((ligne) => section.test(ligne));
78
+ if (debut === -1) {
79
+ throw new Error(`matrice : section ${String(section)} absente du document.`);
80
+ }
81
+ const fin = lignes.findIndex((ligne, indice) => indice > debut && ligne.startsWith('## '));
82
+ const corps = lignes.slice(debut + 1, fin === -1 ? undefined : fin);
83
+ train ??= /`([^`]+)`/.exec(lignes[debut])?.[1] ?? null;
84
+
85
+ for (const [element = '', colonneDesVersions = ''] of lignesDeTableau(corps)) {
86
+ const noms = nomsDeLaCellule(element);
87
+ const versions = [...colonneDesVersions.matchAll(/`([^`]+)`/g)].map(([, version]) => version);
88
+ if (noms.length === 0 || versions.length === 0) {
89
+ throw new Error(`matrice : ligne illisible (${element} | ${colonneDesVersions}).`);
90
+ }
91
+ if (versions.length !== 1 && versions.length !== noms.length) {
92
+ throw new Error(
93
+ `matrice : ${String(noms.length)} noms pour ${String(versions.length)} versions (${element}).`,
94
+ );
95
+ }
96
+ noms.forEach((nom, indice) => {
97
+ const version = versions.length === 1 ? versions[0] : versions[indice];
98
+ if (nom === 'node') nodeMinimal = version.replace(/^>=\s*/, '');
99
+ else if (nom === 'pnpm') pnpm = version;
100
+ else paquets[nom] = version;
101
+ });
102
+ }
103
+ }
104
+
105
+ if (paquets['@angular/core'] === undefined || pnpm === null || nodeMinimal === null) {
106
+ throw new Error('matrice : @angular/core, pnpm ou node manque au document.');
107
+ }
108
+ return { train, paquets, pnpm, nodeMinimal };
109
+ }
110
+
111
+ /** La matrice publiee avec le CLI. */
112
+ export function matriceDuSocle() {
113
+ return lireLaMatrice(readFileSync(cheminDeLaMatrice(), 'utf8'));
114
+ }
115
+
116
+ /** La version du socle que ce CLI porte : la sienne, publiee avec les autres paquets du train. */
117
+ export function versionDuSocle() {
118
+ return JSON.parse(readFileSync(join(ICI, '..', 'package.json'), 'utf8')).version;
119
+ }
package/src/projet.mjs ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Ce que le CLI sait d'une SPA, lu dans ses fichiers et jamais recopie (lot 174).
3
+ *
4
+ * Les huit outils recopies dans chaque SPA ne differaient que par trois ecritures : le nom du remote
5
+ * (`dist/<nom>/browser`, `/ui/<nom>/`), le prefixe de ses selecteurs et `pleinePage`. Le CLI les lit
6
+ * la ou elles vivent deja :
7
+ *
8
+ * - le role : `package.json#starterWeb.role` (`hote` pour la coquille), sinon `remote` des qu'un
9
+ * `manifest.source.json` existe ;
10
+ * - le nom et `pleinePage` : `manifest.source.json` (un remote), `socle` pour l'hote ;
11
+ * - le prefixe : `prefix` du projet d'`angular.json`, ou `starterWeb.prefixe` ;
12
+ * - le dossier livre : `outputPath` de la cible `@angular/build:application`, plus `browser`.
13
+ *
14
+ * Les options de la ligne de commande (`--nom`, `--prefixe`, `--pleine-page`) les remplacent : ce
15
+ * sont les valeurs ATTENDUES, et les etapes signalent tout fichier qui s'en ecarte.
16
+ */
17
+ import { existsSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { lireJson } from './commun.mjs';
20
+
21
+ /** Le projet ne peut pas etre lu : la commande ne peut pas s'executer (code 2). */
22
+ export class ProjetIllisible extends Error {}
23
+
24
+ const CIBLE_DE_CONSTRUCTION = '@angular/build:application';
25
+
26
+ /** Le projet d'application d'`angular.json` : le seul, ou celui qui porte le nom attendu. */
27
+ function projetAngular(angular, nom) {
28
+ const applications = Object.entries(angular?.projects ?? {}).filter(
29
+ ([, projet]) => projet.projectType === undefined || projet.projectType === 'application',
30
+ );
31
+ const retenu =
32
+ applications.find(([cle]) => cle === nom) ??
33
+ (applications.length === 1 ? applications[0] : null);
34
+ if (retenu === null) {
35
+ throw new ProjetIllisible(
36
+ `angular.json : ${String(applications.length)} projets d'application, aucun nomme ${nom}.`,
37
+ );
38
+ }
39
+ return { nomDuProjet: retenu[0], projet: retenu[1] };
40
+ }
41
+
42
+ /** `outputPath` de la cible de construction, plus `browser` : ce qui part vers le serveur. */
43
+ function dossierDeSortie(projet) {
44
+ for (const cible of Object.values(projet.architect ?? {})) {
45
+ if (cible.builder !== CIBLE_DE_CONSTRUCTION) continue;
46
+ const chemin = cible.options?.outputPath;
47
+ if (typeof chemin === 'string') return join(chemin, 'browser');
48
+ if (typeof chemin?.base === 'string') return join(chemin.base, chemin.browser ?? 'browser');
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Le projet sous `racine`. `options` : `nom`, `prefixe`, `pleinePage`, `role` venus de la ligne de
55
+ * commande. Leve `ProjetIllisible` quand un fichier manque ou ne se lit pas.
56
+ */
57
+ export function lireLeProjet(racine, options = {}) {
58
+ const paquet = lireJson(join(racine, 'package.json'));
59
+ if (paquet === null)
60
+ throw new ProjetIllisible(`package.json absent ou illisible dans ${racine}.`);
61
+ const reglages = paquet.starterWeb ?? {};
62
+
63
+ const cheminSource = join(racine, 'manifest.source.json');
64
+ 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)}".`);
67
+ }
68
+ const source = role === 'remote' ? lireJson(cheminSource) : null;
69
+ if (role === 'remote' && source === null) {
70
+ throw new ProjetIllisible('manifest.source.json absent ou illisible : un remote en a un.');
71
+ }
72
+
73
+ const nom = options.nom ?? source?.nom ?? reglages.nom ?? 'socle';
74
+ const angular = lireJson(join(racine, 'angular.json'));
75
+ if (angular === null) throw new ProjetIllisible('angular.json absent ou illisible.');
76
+ const { nomDuProjet, projet } = projetAngular(angular, nom);
77
+ const sortie = dossierDeSortie(projet);
78
+ if (sortie === null) {
79
+ throw new ProjetIllisible(
80
+ `angular.json : aucune cible ${CIBLE_DE_CONSTRUCTION} avec outputPath dans ${nomDuProjet}.`,
81
+ );
82
+ }
83
+
84
+ return {
85
+ racine,
86
+ paquet,
87
+ role,
88
+ nom,
89
+ prefixe: options.prefixe ?? reglages.prefixe ?? projet.prefix ?? null,
90
+ pleinePage: options.pleinePage ?? source?.pleinePage === true,
91
+ source,
92
+ nomDuProjetAngular: nomDuProjet,
93
+ sortieRelative: sortie.split('\\').join('/'),
94
+ sortie: join(racine, sortie),
95
+ };
96
+ }