@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/src/schema.mjs ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Un validateur JSON Schema (2020-12) REDUIT aux mots-cles que le schema du manifeste de remote
3
+ * emploie (`@starterlib/core/schemas/manifeste-de-remote.schema.json`, lot 177).
4
+ *
5
+ * Pourquoi pas une bibliotheque : le CLI n'a aucune dependance a l'execution, et le schema tient en
6
+ * une vingtaine de mots-cles. Un mot-cle que ce validateur ne connait pas LEVE : un schema qui
7
+ * evolue sans que le validateur le lise ne doit jamais passer pour respecte.
8
+ *
9
+ * `format` est une annotation (le vocabulaire par defaut de 2020-12) : il ne juge rien, comme le
10
+ * socle Java et `validerLeManifesteDeRemote`, qui ne demandent a `construitLe` qu'un texte non vide.
11
+ */
12
+
13
+ const ANNOTATIONS = new Set([
14
+ '$schema',
15
+ '$id',
16
+ '$comment',
17
+ '$defs',
18
+ 'title',
19
+ 'description',
20
+ 'format',
21
+ 'examples',
22
+ 'default',
23
+ ]);
24
+ const CONNUS = new Set([
25
+ ...ANNOTATIONS,
26
+ 'type',
27
+ 'const',
28
+ 'enum',
29
+ 'properties',
30
+ 'required',
31
+ 'additionalProperties',
32
+ 'propertyNames',
33
+ 'pattern',
34
+ 'minLength',
35
+ 'minimum',
36
+ 'items',
37
+ 'minItems',
38
+ 'contains',
39
+ 'not',
40
+ 'if',
41
+ 'then',
42
+ 'else',
43
+ '$ref',
44
+ ]);
45
+
46
+ const estUnObjet = (valeur) =>
47
+ typeof valeur === 'object' && valeur !== null && !Array.isArray(valeur);
48
+
49
+ function egaux(a, b) {
50
+ if (a === b) return true;
51
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== 'object') return false;
52
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
53
+ const clesA = Object.keys(a);
54
+ return clesA.length === Object.keys(b).length && clesA.every((cle) => egaux(a[cle], b[cle]));
55
+ }
56
+
57
+ function aLeType(valeur, type) {
58
+ switch (type) {
59
+ case 'object':
60
+ return estUnObjet(valeur);
61
+ case 'array':
62
+ return Array.isArray(valeur);
63
+ case 'string':
64
+ return typeof valeur === 'string';
65
+ case 'boolean':
66
+ return typeof valeur === 'boolean';
67
+ case 'integer':
68
+ return Number.isInteger(valeur);
69
+ case 'number':
70
+ return typeof valeur === 'number' && Number.isFinite(valeur);
71
+ case 'null':
72
+ return valeur === null;
73
+ default:
74
+ throw new Error(`schema : type inconnu "${String(type)}".`);
75
+ }
76
+ }
77
+
78
+ /** Le sous-schema designe par `$ref` (`#/$defs/route`) dans le schema racine. */
79
+ function resoudre(racine, reference) {
80
+ if (!reference.startsWith('#/')) throw new Error(`schema : reference externe "${reference}".`);
81
+ let courant = racine;
82
+ for (const morceau of reference.slice(2).split('/')) {
83
+ courant = courant?.[morceau.replaceAll('~1', '/').replaceAll('~0', '~')];
84
+ }
85
+ if (courant === undefined) throw new Error(`schema : reference introuvable "${reference}".`);
86
+ return courant;
87
+ }
88
+
89
+ function valider(schema, valeur, chemin, racine, problemes) {
90
+ if (schema === true) return;
91
+ if (schema === false) {
92
+ problemes.push(`${chemin} : aucune valeur admise ici.`);
93
+ return;
94
+ }
95
+ for (const cle of Object.keys(schema)) {
96
+ if (!CONNUS.has(cle))
97
+ throw new Error(`schema : mot-cle "${cle}" que le validateur ne lit pas.`);
98
+ }
99
+ const dire = (message) => problemes.push(`${chemin} : ${message}`);
100
+ const respecte = (sousSchema, sousValeur) => {
101
+ const essai = [];
102
+ valider(sousSchema, sousValeur, chemin, racine, essai);
103
+ return essai.length === 0;
104
+ };
105
+
106
+ if (schema.$ref !== undefined)
107
+ valider(resoudre(racine, schema.$ref), valeur, chemin, racine, problemes);
108
+ if (schema.type !== undefined) {
109
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
110
+ if (!types.some((type) => aLeType(valeur, type))) {
111
+ dire(`${types.join(' ou ')} attendu.`);
112
+ return;
113
+ }
114
+ }
115
+ if (schema.const !== undefined && !egaux(valeur, schema.const)) {
116
+ dire(`${JSON.stringify(schema.const)} attendu.`);
117
+ }
118
+ if (schema.enum !== undefined && !schema.enum.some((admis) => egaux(valeur, admis))) {
119
+ dire(
120
+ `une valeur parmi ${schema.enum.map((admis) => JSON.stringify(admis)).join(', ')} attendue.`,
121
+ );
122
+ }
123
+
124
+ if (typeof valeur === 'string') {
125
+ if (schema.pattern !== undefined && !new RegExp(schema.pattern, 'u').test(valeur)) {
126
+ dire(`"${valeur}" ne respecte pas le motif ${schema.pattern}.`);
127
+ }
128
+ if (schema.minLength !== undefined && [...valeur].length < schema.minLength) {
129
+ dire(`${String(schema.minLength)} caractere(s) au moins.`);
130
+ }
131
+ }
132
+ if (typeof valeur === 'number' && schema.minimum !== undefined && valeur < schema.minimum) {
133
+ dire(`${String(schema.minimum)} au moins.`);
134
+ }
135
+
136
+ if (Array.isArray(valeur)) {
137
+ if (schema.minItems !== undefined && valeur.length < schema.minItems) {
138
+ dire(`${String(schema.minItems)} element(s) au moins.`);
139
+ }
140
+ if (schema.items !== undefined) {
141
+ valeur.forEach((element, indice) =>
142
+ valider(schema.items, element, `${chemin}[${String(indice)}]`, racine, problemes),
143
+ );
144
+ }
145
+ if (
146
+ schema.contains !== undefined &&
147
+ !valeur.some((element) => respecte(schema.contains, element))
148
+ ) {
149
+ dire('aucun element ne respecte "contains".');
150
+ }
151
+ }
152
+
153
+ if (estUnObjet(valeur)) {
154
+ const proprietes = schema.properties ?? {};
155
+ for (const cle of schema.required ?? []) {
156
+ if (!Object.hasOwn(valeur, cle)) dire(`propriete "${cle}" obligatoire.`);
157
+ }
158
+ for (const [cle, sousValeur] of Object.entries(valeur)) {
159
+ const sousChemin = chemin === '' ? cle : `${chemin}.${cle}`;
160
+ if (schema.propertyNames !== undefined)
161
+ valider(schema.propertyNames, cle, sousChemin, racine, problemes);
162
+ if (Object.hasOwn(proprietes, cle)) {
163
+ valider(proprietes[cle], sousValeur, sousChemin, racine, problemes);
164
+ } else if (schema.additionalProperties === false) {
165
+ problemes.push(`${sousChemin} : propriete inconnue du schema.`);
166
+ } else if (estUnObjet(schema.additionalProperties)) {
167
+ valider(schema.additionalProperties, sousValeur, sousChemin, racine, problemes);
168
+ }
169
+ }
170
+ }
171
+
172
+ if (schema.not !== undefined && respecte(schema.not, valeur)) {
173
+ dire(
174
+ `forme refusee (not${schema.not.$comment === undefined ? '' : ` : ${schema.not.$comment}`}).`,
175
+ );
176
+ }
177
+ if (schema.if !== undefined) {
178
+ const suite = respecte(schema.if, valeur) ? schema.then : schema.else;
179
+ if (suite !== undefined) valider(suite, valeur, chemin, racine, problemes);
180
+ }
181
+ }
182
+
183
+ /** Les problemes de `valeur` selon `schema` ; `[]` si elle le respecte. Leve sur un schema illisible. */
184
+ export function validerSelonLeSchema(schema, valeur) {
185
+ const problemes = [];
186
+ valider(schema, valeur, '', schema, problemes);
187
+ return problemes.map((probleme) => probleme.replace(/^ : /, 'racine : '));
188
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Ce que la SPA a INSTALLE du socle, lu depuis son `node_modules` et non depuis le CLI (lot 177) :
3
+ * le contrat de federation (`validerLeManifesteDeRemote`) et le schema du manifeste, publies par
4
+ * `@starterlib/core`. Les regles du manifeste n'existent ainsi qu'a un seul endroit, le socle, et la
5
+ * CI d'un remote les applique a la version meme qui tournera dans sa page.
6
+ *
7
+ * Le paquet est trouve par son `package.json` (exporte), puis ses fichiers par leur chemin : le
8
+ * CLI ne depend pas de la forme des `exports` d'une version donnee.
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import { dirname, join } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+
15
+ /** Le dossier installe de `paquet`, depuis la racine de la SPA ; `null` s'il manque. */
16
+ export function dossierInstalle(racine, paquet) {
17
+ try {
18
+ return dirname(createRequire(join(racine, 'package.json')).resolve(`${paquet}/package.json`));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * `{ validerLeManifesteDeRemote, schema, version }` de `@starterlib/core` installe. Leve avec un
26
+ * message qui dit quoi faire si le paquet, le point d'entree ou le schema manque.
27
+ */
28
+ export async function contratDeFederation(racine) {
29
+ const dossier = dossierInstalle(racine, '@starterlib/core');
30
+ if (dossier === null)
31
+ throw new Error("@starterlib/core n'est pas installe : pnpm install d'abord.");
32
+ const module = join(dossier, 'fesm2022', 'starterlib-core-federation.mjs');
33
+ const cheminDuSchema = join(dossier, 'schemas', 'manifeste-de-remote.schema.json');
34
+ for (const [chemin, quoi] of [
35
+ [module, "le point d'entree @starterlib/core/federation"],
36
+ [cheminDuSchema, 'le schema du manifeste'],
37
+ ]) {
38
+ if (!existsSync(chemin))
39
+ throw new Error(`${quoi} manque a @starterlib/core installe (${chemin}).`);
40
+ }
41
+ const { validerLeManifesteDeRemote } = await import(pathToFileURL(module).href);
42
+ if (typeof validerLeManifesteDeRemote !== 'function') {
43
+ throw new Error(
44
+ '@starterlib/core/federation installe ne publie pas validerLeManifesteDeRemote.',
45
+ );
46
+ }
47
+ return {
48
+ validerLeManifesteDeRemote,
49
+ schema: JSON.parse(readFileSync(cheminDuSchema, 'utf8')),
50
+ version: JSON.parse(readFileSync(join(dossier, 'package.json'), 'utf8')).version,
51
+ };
52
+ }
package/src/trains.mjs ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Les trains du socle (ADR web 0006). Avant la 1.0, un train est une mineure (`0.9.x`, puis
3
+ * `0.10.x`) ; ensuite, une majeure.
4
+ */
5
+
6
+ /** `0.10.2` donne `0.10` ; `null` pour ce qui n'est pas un numero de version. */
7
+ export function trainDe(version) {
8
+ const trouve = /^(\d+)\.(\d+)(?:\.\d+)?/.exec(String(version ?? ''));
9
+ return trouve === null ? null : `${trouve[1]}.${trouve[2]}`;
10
+ }
11
+
12
+ /** Negatif si `a` precede `b`, nul s'ils sont egaux : `0.9` precede `0.10`, qui precede `1.0`. */
13
+ export function comparerLesTrains(a, b) {
14
+ const [majeureA, mineureA] = a.split('.').map(Number);
15
+ const [majeureB, mineureB] = b.split('.').map(Number);
16
+ return majeureA - majeureB || mineureA - mineureB;
17
+ }
18
+
19
+ /**
20
+ * Le nombre de trains qui separent `train` de `reference` : 0 s'il est le meme ou en avance.
21
+ *
22
+ * Avant la 1.0, chaque mineure est un train. Apres, chaque majeure. D'un train `0.x` a un train
23
+ * `M.y` (M >= 1), le compte est au moins M : la 1.0 est un train a elle seule, et le detail des
24
+ * mineures `0.x` qui la precedent ne change rien a la decision (un retard de deux trains echoue).
25
+ */
26
+ export function retardEnTrains(train, reference) {
27
+ if (comparerLesTrains(train, reference) >= 0) return 0;
28
+ const [majeure, mineure] = train.split('.').map(Number);
29
+ const [majeureDeReference, mineureDeReference] = reference.split('.').map(Number);
30
+ if (majeure === 0 && majeureDeReference === 0) return mineureDeReference - mineure;
31
+ if (majeure >= 1) return majeureDeReference - majeure;
32
+ return majeureDeReference;
33
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `starter-web verify` : les etapes d'une SPA, dans l'ordre du plan SPA §3, arretees a la premiere
3
+ * qui echoue — et qui la NOMME : un « Command failed » ne dit pas quoi corriger.
4
+ *
5
+ * versions -> dependances -> jetons -> format -> lint -> i18n -> test -> contrat -> build ->
6
+ * manifeste -> federation -> entree (hote) -> budget -> amorcage (hote) -> securite.
7
+ *
8
+ * Le sprite est pose une fois, avant la premiere etape qui lit le code (lint, test, build).
9
+ */
10
+ import { construire } from './construction/construire.mjs';
11
+ import { poserLeSprite } from './construction/sprite.mjs';
12
+ import budget from './etapes/budget.mjs';
13
+ import dependances from './etapes/dependances.mjs';
14
+ import federation from './etapes/federation.mjs';
15
+ import { amorcage, entree } from './etapes/hote.mjs';
16
+ import i18n from './etapes/i18n.mjs';
17
+ import jetons from './etapes/jetons.mjs';
18
+ import manifeste from './etapes/manifeste.mjs';
19
+ import { contrat, format, lint, test } from './etapes/outillage.mjs';
20
+ import securite from './etapes/securite.mjs';
21
+ import versions from './etapes/versions.mjs';
22
+
23
+ const build = {
24
+ nom: 'build',
25
+ roles: ['remote', 'hote'],
26
+ preparation: 'sprite',
27
+ executer(projet, contexte) {
28
+ const code = construire(projet, { journal: contexte.journal, sprite: false });
29
+ return code === 0
30
+ ? { defauts: [], resume: 'construction de production conforme.' }
31
+ : { defauts: [`construction en echec (code ${String(code)}).`] };
32
+ },
33
+ };
34
+
35
+ /** Les etapes, dans leur ordre. */
36
+ export const ETAPES = [
37
+ versions,
38
+ dependances,
39
+ jetons,
40
+ format,
41
+ lint,
42
+ i18n,
43
+ test,
44
+ contrat,
45
+ build,
46
+ manifeste,
47
+ federation,
48
+ entree,
49
+ budget,
50
+ amorcage,
51
+ securite,
52
+ ];
53
+
54
+ /** Les noms d'etape inconnus d'une liste. */
55
+ export function etapesInconnues(noms) {
56
+ return noms.filter((nom) => !ETAPES.some((etape) => etape.nom === nom));
57
+ }
58
+
59
+ /**
60
+ * Lance les etapes retenues sur `projet`. `options` : `etapes` (seulement celles-ci), `sauter`,
61
+ * `trainDeReference`, `journal` (`{ info, erreur }`). Rend 0 si tout est conforme, 1 sinon.
62
+ */
63
+ export async function verifier(
64
+ projet,
65
+ { etapes, sauter = [], trainDeReference, journal = console } = {},
66
+ ) {
67
+ const retenues = ETAPES.filter(
68
+ (etape) =>
69
+ etape.roles.includes(projet.role) &&
70
+ (etapes === undefined || etapes.includes(etape.nom)) &&
71
+ !sauter.includes(etape.nom),
72
+ );
73
+ const contexte = { trainDeReference, journal };
74
+ let spritePose = false;
75
+
76
+ for (const [indice, etape] of retenues.entries()) {
77
+ journal.info(`[${String(indice + 1)}/${String(retenues.length)}] ${etape.nom}`);
78
+ if (etape.preparation === 'sprite' && !spritePose) {
79
+ journal.info(` sprite : ${poserLeSprite(projet)}`);
80
+ spritePose = true;
81
+ }
82
+ let resultat;
83
+ try {
84
+ resultat = await etape.executer(projet, contexte);
85
+ } catch (erreur) {
86
+ resultat = { defauts: [erreur instanceof Error ? erreur.message : String(erreur)] };
87
+ }
88
+ if (resultat.sautee !== undefined) {
89
+ journal.info(` sautee : ${resultat.sautee}`);
90
+ continue;
91
+ }
92
+ for (const ligne of resultat.details ?? []) journal.info(` ${ligne}`);
93
+ for (const avertissement of resultat.avertissements ?? []) journal.info(` ! ${avertissement}`);
94
+ const defauts = resultat.defauts ?? [];
95
+ if (defauts.length > 0) {
96
+ for (const defaut of defauts) journal.erreur(` - ${defaut}`);
97
+ journal.erreur(
98
+ `starter-web verify : echec a l'etape ${etape.nom} (${String(defauts.length)} defaut(s)).`,
99
+ );
100
+ return 1;
101
+ }
102
+ if (resultat.resume !== undefined) journal.info(` ${etape.nom} : ${resultat.resume}`);
103
+ }
104
+ journal.info(`starter-web verify : ${String(retenues.length)} etape(s) conforme(s).`);
105
+ return 0;
106
+ }