@roomi-fields/grillhouse 0.0.0-stage → 0.23.0
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/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The summary of the circuit's state of a ticket, bounded, as `kubectl describe` or `gh run view`
|
|
3
|
+
// give a view of an object in place of its raw record: the regulator reads the state by it (its
|
|
4
|
+
// instruction names it), since the state, its report carrying the detail of a final pass, can pass
|
|
5
|
+
// the limit of the tool Read (BPScript 320.4: 33 k tokens, past the 25 k of Read).
|
|
6
|
+
//
|
|
7
|
+
// npx grillhouse etat-du-circuit <état> <état> the path of `<copie>.circuit.json`
|
|
8
|
+
//
|
|
9
|
+
// prints, in BORNE characters at most: the step (« 2.3 Code ») and its component, its attempt and
|
|
10
|
+
// its loops; the stop, its name and code; the components of the title and those reached; the
|
|
11
|
+
// returns kept for a component; the last reds of the report (its lines `- …`); the head of the
|
|
12
|
+
// report; and the files: the ticket's log, its proofs (the last ones), its sheets, and what the
|
|
13
|
+
// copy changes against HEAD (`git status --porcelain`). Each line is cut to LIGNE characters. An
|
|
14
|
+
// unreadable state is said on stderr, exit 1.
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { etapeDuCircuit, libelle } from './etapes.mjs';
|
|
20
|
+
|
|
21
|
+
// The bound of the summary, in characters: under the 25 000 tokens of Read at any count of
|
|
22
|
+
// characters per token above 1.
|
|
23
|
+
export const BORNE = 20_000;
|
|
24
|
+
const LIGNE = 300;
|
|
25
|
+
const ROUGES = 20;
|
|
26
|
+
const TETE = 30;
|
|
27
|
+
const PREUVES = 3;
|
|
28
|
+
const CHANGES = 40;
|
|
29
|
+
|
|
30
|
+
const couper = l => (l.length > LIGNE ? `${l.slice(0, LIGNE - 1)}…` : l);
|
|
31
|
+
// The <n> first (or, <fin>, last) items of <liste>, the rest counted.
|
|
32
|
+
function borner(liste, n, fin = false) {
|
|
33
|
+
if (liste.length <= n) return liste;
|
|
34
|
+
const reste = `… ${liste.length - n} de plus`;
|
|
35
|
+
return fin ? [reste, ...liste.slice(-n)] : [...liste.slice(0, n), reste];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The summary of the state in the file <fichier>; throws when it cannot be read.
|
|
39
|
+
export function resumeDeLEtat(fichier) {
|
|
40
|
+
const etat = JSON.parse(readFileSync(fichier, 'utf8'));
|
|
41
|
+
const copie = fichier.replace(/\.circuit\.json$/, '');
|
|
42
|
+
const ticket = path.basename(copie);
|
|
43
|
+
const e = etapeDuCircuit(etat);
|
|
44
|
+
const rapport = String(etat.rapport ?? '')
|
|
45
|
+
.split('\n')
|
|
46
|
+
.map(l => l.trimEnd());
|
|
47
|
+
const rouges = rapport.filter(l => /^\s*- /.test(l)).map(l => l.trim());
|
|
48
|
+
const dossier = path.dirname(fichier);
|
|
49
|
+
const preuves = existsSync(dossier)
|
|
50
|
+
? readdirSync(dossier)
|
|
51
|
+
.map(f => [
|
|
52
|
+
f,
|
|
53
|
+
new RegExp(
|
|
54
|
+
`^${ticket.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.preuve-(\\d+)\\.md$`
|
|
55
|
+
).exec(f),
|
|
56
|
+
])
|
|
57
|
+
.filter(([, m]) => m)
|
|
58
|
+
.sort(([, a], [, b]) => Number(a[1]) - Number(b[1]))
|
|
59
|
+
.map(([f]) => path.join(dossier, f))
|
|
60
|
+
: [];
|
|
61
|
+
const statut = existsSync(copie)
|
|
62
|
+
? spawnSync('git', ['-C', copie, 'status', '--porcelain'], { encoding: 'utf8' })
|
|
63
|
+
: null;
|
|
64
|
+
const changes = statut?.status === 0 ? statut.stdout.split('\n').filter(Boolean) : [];
|
|
65
|
+
const renvois = Object.entries(etat.renvois ?? {}).map(
|
|
66
|
+
([c, r]) => `- ${c} → ${r?.etape ?? '?'} : ${String(r?.[r?.etape] ?? '').replace(/\s+/g, ' ')}`
|
|
67
|
+
);
|
|
68
|
+
const lignes = [
|
|
69
|
+
`L'ÉTAT DU CIRCUIT de ${ticket} (${fichier})`,
|
|
70
|
+
'',
|
|
71
|
+
`Étape : ${e ? libelle(e) : etat.etape} — composant ${etat.composant ?? 'aucun'} — tentative ${etat.tentative ?? 0}${etat.boucles ? ` — boucles ${JSON.stringify(etat.boucles)}` : ''}`,
|
|
72
|
+
`Arrêt : ${etat.raison ?? 'aucun'} (code ${etat.code ?? 'aucun'})`,
|
|
73
|
+
`Composants du titre : ${(etat.composants ?? []).join(', ') || 'aucun'}`,
|
|
74
|
+
`Atteints : ${(etat.atteints ?? []).join(', ') || 'aucun'}`,
|
|
75
|
+
...(renvois.length ? ['', 'Renvois :', ...renvois] : []),
|
|
76
|
+
'',
|
|
77
|
+
`Les derniers rouges du rapport (${rouges.length}) :`,
|
|
78
|
+
...(rouges.length ? borner(rouges, ROUGES, true) : ['aucun']),
|
|
79
|
+
'',
|
|
80
|
+
`Le rapport, sa tête (${rapport.length} lignes ; le détail est au journal) :`,
|
|
81
|
+
...borner(rapport, TETE),
|
|
82
|
+
'',
|
|
83
|
+
'Les fichiers :',
|
|
84
|
+
`- le journal : ${etat.journal ?? `${copie}.log`}`,
|
|
85
|
+
...borner(preuves, PREUVES, true).map(p => (p.startsWith('…') ? p : `- une preuve : ${p}`)),
|
|
86
|
+
`- les fiches : ${copie}.fiches.jsonl`,
|
|
87
|
+
`- ce que la copie change (${changes.length}) :`,
|
|
88
|
+
...(changes.length ? borner(changes, CHANGES) : ['aucun']),
|
|
89
|
+
].map(couper);
|
|
90
|
+
let texte = `${lignes.join('\n')}\n`;
|
|
91
|
+
if (texte.length > BORNE) texte = `${texte.slice(0, BORNE - 2)}…\n`;
|
|
92
|
+
return texte;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
96
|
+
const fichier = process.argv[2];
|
|
97
|
+
try {
|
|
98
|
+
if (!fichier) throw new Error('usage : npx grillhouse etat-du-circuit <état>');
|
|
99
|
+
process.stdout.write(resumeDeLEtat(path.resolve(fichier)));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
process.stderr.write(`grillhouse etat-du-circuit : ${fichier ?? ''} illisible : ${e.message}\n`);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The boundaries of the code, checked by dependency-cruiser. Its configuration
|
|
3
|
+
// (`.dependency-cruiser.cjs`) is generated from the components of the repository, like the index
|
|
4
|
+
// of the interfaces; a project adds its own rules and options in `.dependency-cruiser.projet.cjs`
|
|
5
|
+
// (`module.exports = { forbidden: [...], options: {...} }`), which the generated file reads when
|
|
6
|
+
// dependency-cruiser loads it. The file brings every key of dependency-cruiser's configuration:
|
|
7
|
+
// its `forbidden`, `allowed` and `required` rules add to the frame's; `allowedSeverity` and
|
|
8
|
+
// `extends` pass as they are. Its `exclude` and `doNotFollow` patterns add to the frame's; any
|
|
9
|
+
// other option replaces the frame's at its first level, as the `extends` of dependency-cruiser does.
|
|
10
|
+
//
|
|
11
|
+
// npx grillhouse frontieres refuses a stale configuration, then runs dependency-cruiser
|
|
12
|
+
// npx grillhouse frontieres --ecrire writes the configuration
|
|
13
|
+
//
|
|
14
|
+
// The rules: no cycle; a package reaches another by its name, never by a path into its tree; a
|
|
15
|
+
// module of `src/` reaches another by its entry (`index`); the code of `src/` imports no test; the
|
|
16
|
+
// published code imports no development dependency, whether it imports values or only types: it
|
|
17
|
+
// is the code of `src/` outside the test files, and every file that the `exports` of a package
|
|
18
|
+
// reach (each target of a condition, the exported test material included) with the files these
|
|
19
|
+
// import; every import resolves (a path a package does not export is refused); an orphan file is
|
|
20
|
+
// reported, without refusing, unless it is an entry: an index, a configuration, a declaration, a
|
|
21
|
+
// test, or a file that the `bin` field of a package names (its commands, as knip takes them). Who uses which component is the consumers check's
|
|
22
|
+
// (`scripts/consommateurs.mjs`). The files of a package out of the frame
|
|
23
|
+
// (`docs/agents/hors-cadre.txt`) are excluded, and no module rule is written for it.
|
|
24
|
+
//
|
|
25
|
+
// An error of dependency-cruiser inscribed in the project's debt (`grillhouse-dette.json`, see
|
|
26
|
+
// `dette.mjs`) passes, keyed by the file it starts from and the rule's name; the report, written by
|
|
27
|
+
// dependency-cruiser's `err-long` reporter, ignores the errors the debt covers as known violations
|
|
28
|
+
// (`--ignore-known`). A stale configuration and an entry without a source are never inscribed.
|
|
29
|
+
//
|
|
30
|
+
// The published code is found before dependency-cruiser runs: each package under `packages/`
|
|
31
|
+
// names its entries, the targets of its `exports`; an entry is a TypeScript source, a JavaScript
|
|
32
|
+
// source that no `tsconfig*.json` of the package emits (a package written in JavaScript, the
|
|
33
|
+
// frame's own), or a built file that comes back to its sources by what TypeScript emits with the
|
|
34
|
+
// `tsconfig*.json` of the package; a declaration (`.d.ts`) or a JSON file carries no code and is
|
|
35
|
+
// no entry; an entry that is none of these is refused, named. A first run of dependency-cruiser gives
|
|
36
|
+
// the files these sources import; the second judges them, through the file that
|
|
37
|
+
// `GRILLHOUSE_JUGES` names, which the generated configuration reads.
|
|
38
|
+
import { spawnSync } from 'node:child_process';
|
|
39
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
40
|
+
import { createRequire } from 'node:module';
|
|
41
|
+
import { tmpdir } from 'node:os';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import { pathToFileURL } from 'node:url';
|
|
44
|
+
import { composants } from './consommateurs.mjs';
|
|
45
|
+
import { horsCadre } from './paquets.mjs';
|
|
46
|
+
import { trancher } from './dette.mjs';
|
|
47
|
+
|
|
48
|
+
export const CONFIGURATION = '.dependency-cruiser.cjs';
|
|
49
|
+
export const RACINES = ['src', 'packages'];
|
|
50
|
+
const TESTS = '(^|/)(test|tests|__tests__)/|[.](test|spec)[.][a-z]+$';
|
|
51
|
+
const CODE = '^src/|^packages/[^/]+/src/';
|
|
52
|
+
const REGLE_OUTILS = 'pas-d-outil-de-developpement-dans-le-code';
|
|
53
|
+
// The environment variable naming the file that holds the pattern of the published files.
|
|
54
|
+
const JUGES = 'GRILLHOUSE_JUGES';
|
|
55
|
+
|
|
56
|
+
// The `exclude` patterns of the configuration: built files, and the directories under `src/` and
|
|
57
|
+
// `packages/` that the list out of the frame names.
|
|
58
|
+
function exclus(racine) {
|
|
59
|
+
const motifs = ['^(dist|build|coverage)/', '^packages/[^/]+/(dist|build|coverage)/'];
|
|
60
|
+
for (const nom of [...horsCadre(racine)].sort()) {
|
|
61
|
+
motifs.push(`^(packages|src)/${nom.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`);
|
|
62
|
+
}
|
|
63
|
+
return motifs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The width of a line of the generated configuration, the project's (`.prettierrc`).
|
|
67
|
+
const LARGEUR = 100;
|
|
68
|
+
|
|
69
|
+
// A rule as JavaScript lines at indent <retrait>, each within LARGEUR columns: one key per line,
|
|
70
|
+
// a string too long for its line cut at a space, a `|` or the width into pieces joined by `+`.
|
|
71
|
+
function ecrire(regle, retrait) {
|
|
72
|
+
const lignes = JSON.stringify(regle, null, 2).split('\n');
|
|
73
|
+
return lignes
|
|
74
|
+
.map(l => {
|
|
75
|
+
const ligne = retrait + l;
|
|
76
|
+
const m = /^(\s*(?:"[^"]*": )?)"(.*)"(,?)$/.exec(ligne);
|
|
77
|
+
if (ligne.length <= LARGEUR || !m) return ligne;
|
|
78
|
+
const [, tete, valeur, virgule] = m;
|
|
79
|
+
const pieces = [];
|
|
80
|
+
let reste = valeur;
|
|
81
|
+
const place = LARGEUR - tete.length - 6;
|
|
82
|
+
while (reste.length > place) {
|
|
83
|
+
const dans = reste.slice(0, place);
|
|
84
|
+
let coupe = Math.max(dans.lastIndexOf(' '), dans.lastIndexOf('|')) + 1;
|
|
85
|
+
if (coupe === 0) coupe = place;
|
|
86
|
+
while (reste[coupe - 1] === '\\') coupe -= 1;
|
|
87
|
+
pieces.push(reste.slice(0, coupe));
|
|
88
|
+
reste = reste.slice(coupe);
|
|
89
|
+
}
|
|
90
|
+
pieces.push(reste);
|
|
91
|
+
const pas = `${/^\s*/.exec(tete)[0]} `;
|
|
92
|
+
return `${tete}"${pieces.join(`" +\n${pas}"`)}"${virgule}`;
|
|
93
|
+
})
|
|
94
|
+
.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The patterns of the files that the `bin` field of a package of <racine> names (a path, or an
|
|
98
|
+
// object of paths), each from the root: entries of the package, as knip takes them.
|
|
99
|
+
function entreesBin(racine) {
|
|
100
|
+
return composants(racine).flatMap(({ dir }) => {
|
|
101
|
+
const fichier = path.join(dir, 'package.json');
|
|
102
|
+
if (!existsSync(fichier)) return [];
|
|
103
|
+
const { bin } = JSON.parse(readFileSync(fichier, 'utf8'));
|
|
104
|
+
const cibles = typeof bin === 'string' ? [bin] : Object.values(bin ?? {});
|
|
105
|
+
return cibles
|
|
106
|
+
.filter(c => typeof c === 'string')
|
|
107
|
+
.map(c => `^${echappe(posix(path.relative(racine, path.resolve(dir, c))))}$`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The configuration text for the repository at <racine>.
|
|
112
|
+
export function configuration(racine) {
|
|
113
|
+
const modules = composants(racine).filter(c => path.relative(racine, c.dir).startsWith('src/'));
|
|
114
|
+
const bin = [...new Set(entreesBin(racine))].sort();
|
|
115
|
+
const regles = [
|
|
116
|
+
{
|
|
117
|
+
name: 'pas-de-cycle',
|
|
118
|
+
comment: 'aucun module ne dépend de lui-même par un détour',
|
|
119
|
+
severity: 'error',
|
|
120
|
+
from: {},
|
|
121
|
+
to: { circular: true },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'un-paquet-par-son-nom',
|
|
125
|
+
comment: 'un paquet en importe un autre par son nom, jamais par un chemin dans son arbre',
|
|
126
|
+
severity: 'error',
|
|
127
|
+
from: { path: '^packages/([^/]+)/' },
|
|
128
|
+
to: { path: '^packages/([^/]+)/', pathNot: '^packages/$1/' },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'pas-de-test-dans-le-code',
|
|
132
|
+
comment: "le code livré n'importe aucun test",
|
|
133
|
+
severity: 'error',
|
|
134
|
+
from: { path: CODE, pathNot: TESTS },
|
|
135
|
+
to: { path: TESTS },
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: REGLE_OUTILS,
|
|
139
|
+
comment:
|
|
140
|
+
"le code livré (src et ce que les exports du paquet atteignent) n'importe aucune " +
|
|
141
|
+
'dépendance de développement, types compris',
|
|
142
|
+
severity: 'error',
|
|
143
|
+
from: { path: CODE, pathNot: TESTS },
|
|
144
|
+
to: { dependencyTypes: ['npm-dev'] },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'tout-import-se-resout',
|
|
148
|
+
comment: "un import se résout : un chemin qu'un paquet n'exporte pas est refusé",
|
|
149
|
+
severity: 'error',
|
|
150
|
+
from: {},
|
|
151
|
+
to: { couldNotResolve: true },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'pas-d-orphelin',
|
|
155
|
+
comment: "un fichier que rien n'importe est signalé",
|
|
156
|
+
severity: 'warn',
|
|
157
|
+
from: {
|
|
158
|
+
orphan: true,
|
|
159
|
+
pathNot: [`${TESTS}|(^|/)index[.][a-z]+$|[.]d[.]ts$|[.]config[.][a-z]+$`, ...bin].join('|'),
|
|
160
|
+
},
|
|
161
|
+
to: {},
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
if (modules.length) {
|
|
165
|
+
regles.splice(2, 0, {
|
|
166
|
+
name: 'un-module-par-son-entree',
|
|
167
|
+
comment:
|
|
168
|
+
"un module de src en importe un autre par son entrée (index), jamais par l'intérieur",
|
|
169
|
+
severity: 'error',
|
|
170
|
+
from: { path: '^src/([^/]+)/' },
|
|
171
|
+
to: { path: '^src/([^/]+)/', pathNot: '^src/$1/|^src/[^/]+/index[.][a-z]+$' },
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const tsconfig = existsSync(path.join(racine, 'tsconfig.json'));
|
|
175
|
+
return `// Généré par \`npx grillhouse frontieres --ecrire\` ; ne pas éditer. Les règles et les options
|
|
176
|
+
// propres au projet vivent dans .dependency-cruiser.projet.cjs, lu au chargement.
|
|
177
|
+
const fs = require('node:fs');
|
|
178
|
+
const projet = fs.existsSync(\`\${__dirname}/.dependency-cruiser.projet.cjs\`)
|
|
179
|
+
? require('./.dependency-cruiser.projet.cjs')
|
|
180
|
+
: {};
|
|
181
|
+
|
|
182
|
+
// The non-empty patterns of a filter written in any form dependency-cruiser accepts.
|
|
183
|
+
const motifs = f => {
|
|
184
|
+
if (typeof f === 'string') return [f];
|
|
185
|
+
if (Array.isArray(f)) return f;
|
|
186
|
+
return f && typeof f === 'object' ? motifs(f.path) : [];
|
|
187
|
+
};
|
|
188
|
+
// A filter of the frame plus the project's: the patterns add up, the other keys are the project's.
|
|
189
|
+
const ajoute = (cadre, propre) => {
|
|
190
|
+
const ajouts = motifs(propre).filter(Boolean);
|
|
191
|
+
const reste = propre && typeof propre === 'object' && !Array.isArray(propre) ? propre : {};
|
|
192
|
+
return { ...reste, path: [cadre.path, ...ajouts].join('|') };
|
|
193
|
+
};
|
|
194
|
+
// The published files that the script found (a pattern, in the file ${JUGES} names)
|
|
195
|
+
// replace the default scope of the rule that refuses a development dependency.
|
|
196
|
+
const fichierJuges = process.env.${JUGES};
|
|
197
|
+
const juges = fichierJuges ? fs.readFileSync(fichierJuges, 'utf8') : null;
|
|
198
|
+
const portee = regle =>
|
|
199
|
+
regle.name === '${REGLE_OUTILS}' && juges !== null
|
|
200
|
+
? { ...regle, from: { path: juges } }
|
|
201
|
+
: regle;
|
|
202
|
+
const cadre = {
|
|
203
|
+
doNotFollow: { path: 'node_modules' },
|
|
204
|
+
exclude: {
|
|
205
|
+
path: [
|
|
206
|
+
${exclus(racine)
|
|
207
|
+
.map(m => ` '${m.replace(/[\\']/g, '\\$&')}',`)
|
|
208
|
+
.join('\n')}
|
|
209
|
+
].join('|'),
|
|
210
|
+
},
|
|
211
|
+
tsPreCompilationDeps: true,
|
|
212
|
+
preserveSymlinks: true,
|
|
213
|
+
${tsconfig ? " tsConfig: { fileName: 'tsconfig.json' },\n" : ''} enhancedResolveOptions: {
|
|
214
|
+
exportsFields: ['exports'],
|
|
215
|
+
conditionNames: ['import', 'require', 'default', 'types'],
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
forbidden: [
|
|
221
|
+
...[
|
|
222
|
+
${regles.map(r => `${ecrire(r, ' ')},`).join('\n')}
|
|
223
|
+
].map(portee),
|
|
224
|
+
...(projet.forbidden ?? []),
|
|
225
|
+
],
|
|
226
|
+
...(projet.allowed ? { allowed: projet.allowed } : {}),
|
|
227
|
+
...(projet.required ? { required: projet.required } : {}),
|
|
228
|
+
...(projet.allowedSeverity ? { allowedSeverity: projet.allowedSeverity } : {}),
|
|
229
|
+
...(projet.extends ? { extends: projet.extends } : {}),
|
|
230
|
+
options: {
|
|
231
|
+
...cadre,
|
|
232
|
+
...projet.options,
|
|
233
|
+
doNotFollow: ajoute(cadre.doNotFollow, projet.options?.doNotFollow),
|
|
234
|
+
exclude: ajoute(cadre.exclude, projet.options?.exclude),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---- The published code -------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
const requis = createRequire(import.meta.url);
|
|
243
|
+
const posix = chemin => chemin.split(path.sep).join('/');
|
|
244
|
+
const echappe = texte => texte.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
245
|
+
const DECLARATION = /[.]d[.]ts$/;
|
|
246
|
+
const SOURCE_TS = /[.]tsx?$/;
|
|
247
|
+
const SOURCE_JS = /[.][mc]?js$/;
|
|
248
|
+
|
|
249
|
+
// What the `tsconfig*.json` of the package at <dir> emit, as TypeScript reads them (their
|
|
250
|
+
// `extends` included) and computes their output (`getOutputFileNames`): a Map from the absolute
|
|
251
|
+
// path of an emitted file to the sources it comes from.
|
|
252
|
+
export function sorties(dir) {
|
|
253
|
+
const ts = requis('typescript');
|
|
254
|
+
const hote = { ...ts.sys, onUnRecoverableConfigFileDiagnostic: () => {} };
|
|
255
|
+
const table = new Map();
|
|
256
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
257
|
+
if (!e.isFile() || !/^tsconfig.*[.]json$/.test(e.name)) continue;
|
|
258
|
+
const analyse = ts.getParsedCommandLineOfConfigFile(path.join(dir, e.name), {}, hote);
|
|
259
|
+
if (!analyse || analyse.options.noEmit) continue;
|
|
260
|
+
for (const source of analyse.fileNames.map(f => path.resolve(f))) {
|
|
261
|
+
if (DECLARATION.test(source) || !existsSync(source)) continue;
|
|
262
|
+
const emises = ts.getOutputFileNames(analyse, source, ts.sys.useCaseSensitiveFileNames);
|
|
263
|
+
for (const sortie of emises) {
|
|
264
|
+
const chemin = path.resolve(sortie);
|
|
265
|
+
if (chemin !== source) table.set(chemin, new Set([...(table.get(chemin) ?? []), source]));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return table;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The targets of an `exports` value: the string itself, or the targets of each value of an object
|
|
273
|
+
// (its subpaths and its conditions).
|
|
274
|
+
function cibles(valeur) {
|
|
275
|
+
if (typeof valeur === 'string') return [valeur];
|
|
276
|
+
return valeur && typeof valeur === 'object' ? Object.values(valeur).flatMap(cibles) : [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// The source files published by the package at <dir>, and the entries without a source:
|
|
280
|
+
// { sources: Set of absolute paths, refus: [message] }. An entry is its own source when it is a
|
|
281
|
+
// TypeScript source that exists, or a JavaScript source that exists and TypeScript does not emit,
|
|
282
|
+
// else its sources are those TypeScript emits it from; a declaration or a JSON file is skipped.
|
|
283
|
+
function sourcesDuPaquet(racine, dir) {
|
|
284
|
+
const sources = new Set();
|
|
285
|
+
const refus = [];
|
|
286
|
+
const fichier = path.join(dir, 'package.json');
|
|
287
|
+
if (!existsSync(fichier)) return { sources, refus };
|
|
288
|
+
const entrees = cibles(JSON.parse(readFileSync(fichier, 'utf8')).exports);
|
|
289
|
+
if (entrees.length === 0) return { sources, refus };
|
|
290
|
+
const emis = sorties(dir);
|
|
291
|
+
for (const cible of entrees) {
|
|
292
|
+
const abs = path.resolve(dir, cible);
|
|
293
|
+
if (DECLARATION.test(abs) || abs.endsWith('.json')) continue;
|
|
294
|
+
const source =
|
|
295
|
+
SOURCE_TS.test(abs) || (SOURCE_JS.test(abs) && !emis.has(abs) && existsSync(abs));
|
|
296
|
+
const trouvees = source ? [abs].filter(f => existsSync(f)) : [...(emis.get(abs) ?? [])];
|
|
297
|
+
if (trouvees.length === 0) {
|
|
298
|
+
refus.push(
|
|
299
|
+
`⛔ ${posix(path.relative(racine, fichier))} : l'entrée ${cible} n'a pas de source — ` +
|
|
300
|
+
'ni fichier source, ni configuration TypeScript du paquet qui la produise.'
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
for (const f of trouvees) sources.add(f);
|
|
304
|
+
}
|
|
305
|
+
return { sources, refus };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// The sources published by the packages of the repository at <racine>, as paths relative to
|
|
309
|
+
// <racine>, with the entries refused: { sources: Set, refus: [message] }.
|
|
310
|
+
export function publiees(racine) {
|
|
311
|
+
const sources = new Set();
|
|
312
|
+
const refus = [];
|
|
313
|
+
for (const { dir } of composants(racine)) {
|
|
314
|
+
const trouve = sourcesDuPaquet(racine, dir);
|
|
315
|
+
for (const f of trouve.sources) sources.add(posix(path.relative(racine, f)));
|
|
316
|
+
refus.push(...trouve.refus);
|
|
317
|
+
}
|
|
318
|
+
return { sources, refus };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// One run of dependency-cruiser over <racines>; `json` gives the graph on the standard output.
|
|
322
|
+
// <connues>, when given, names the file of the known violations dependency-cruiser ignores
|
|
323
|
+
// (`--ignore-known`).
|
|
324
|
+
function depcruise(racine, racines, type, environnement, connues = null) {
|
|
325
|
+
const json = type === 'json';
|
|
326
|
+
const ignorees = connues ? ['--ignore-known', connues] : [];
|
|
327
|
+
return spawnSync(
|
|
328
|
+
'npx',
|
|
329
|
+
[
|
|
330
|
+
'--no-install',
|
|
331
|
+
'depcruise',
|
|
332
|
+
...racines,
|
|
333
|
+
'--config',
|
|
334
|
+
CONFIGURATION,
|
|
335
|
+
'--output-type',
|
|
336
|
+
type,
|
|
337
|
+
...ignorees,
|
|
338
|
+
],
|
|
339
|
+
{
|
|
340
|
+
cwd: racine,
|
|
341
|
+
env: environnement,
|
|
342
|
+
encoding: 'utf8',
|
|
343
|
+
stdio: json ? ['ignore', 'pipe', 'inherit'] : 'inherit',
|
|
344
|
+
maxBuffer: 1024 * 1024 * 512,
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// The files the rule judges, from the graph that dependency-cruiser gives: the code of `src/`
|
|
350
|
+
// outside the test files, and every file reached from a published source without crossing
|
|
351
|
+
// node_modules.
|
|
352
|
+
function aJuger(graphe, sources) {
|
|
353
|
+
const modules = new Map(graphe.modules.map(m => [m.source, m]));
|
|
354
|
+
const juges = new Set(
|
|
355
|
+
[...modules.keys()].filter(f => new RegExp(CODE).test(f) && !new RegExp(TESTS).test(f))
|
|
356
|
+
);
|
|
357
|
+
const atteints = new Set();
|
|
358
|
+
const pile = [...sources];
|
|
359
|
+
while (pile.length) {
|
|
360
|
+
const f = pile.pop();
|
|
361
|
+
if (atteints.has(f)) continue;
|
|
362
|
+
atteints.add(f);
|
|
363
|
+
juges.add(f);
|
|
364
|
+
for (const d of modules.get(f)?.dependencies ?? []) {
|
|
365
|
+
if (d.couldNotResolve || d.coreModule || /(^|\/)node_modules\//.test(d.resolved)) continue;
|
|
366
|
+
pile.push(d.resolved);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return juges;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The errors of a cruise result as findings the debt (`dette.mjs`) judges: keyed by the file a
|
|
373
|
+
// violation starts from and its rule's name, the violation kept to be reported.
|
|
374
|
+
export function constats(resultat) {
|
|
375
|
+
return resultat.summary.violations
|
|
376
|
+
.filter(v => v.rule.severity === 'error')
|
|
377
|
+
.map(v => ({
|
|
378
|
+
fichier: v.from,
|
|
379
|
+
regle: v.rule.name,
|
|
380
|
+
message: `${v.rule.name}: ${v.from} → ${v.to}`,
|
|
381
|
+
violation: v,
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The pattern of the files to judge, as dependency-cruiser's `path` filter reads it.
|
|
386
|
+
const motifDes = fichiers => `^(${[...fichiers].sort().map(echappe).join('|')})$`;
|
|
387
|
+
|
|
388
|
+
// The command on the repository at <racine>, with <args>: its exit code.
|
|
389
|
+
function frontieres(racine, args) {
|
|
390
|
+
const attendu = configuration(racine);
|
|
391
|
+
const cible = path.join(racine, CONFIGURATION);
|
|
392
|
+
if (args.includes('--ecrire')) {
|
|
393
|
+
writeFileSync(cible, attendu);
|
|
394
|
+
return 0;
|
|
395
|
+
}
|
|
396
|
+
const present = existsSync(cible) ? readFileSync(cible, 'utf8') : '';
|
|
397
|
+
if (present !== attendu) {
|
|
398
|
+
process.stderr.write(
|
|
399
|
+
`⛔ ${CONFIGURATION} n'est pas à jour avec les composants : npx grillhouse frontieres --ecrire le régénère.\n`
|
|
400
|
+
);
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
// An entry without a source stops the check before dependency-cruiser runs.
|
|
404
|
+
const { sources, refus } = publiees(racine);
|
|
405
|
+
if (refus.length > 0) {
|
|
406
|
+
process.stderr.write(`${refus.join('\n')}\n`);
|
|
407
|
+
return 1;
|
|
408
|
+
}
|
|
409
|
+
const racines = RACINES.filter(r => existsSync(path.join(racine, r)));
|
|
410
|
+
if (racines.length === 0) return 0;
|
|
411
|
+
const environnement = { ...process.env };
|
|
412
|
+
delete environnement[JUGES];
|
|
413
|
+
let dossier = null;
|
|
414
|
+
try {
|
|
415
|
+
if (sources.size > 0) {
|
|
416
|
+
const graphe = depcruise(racine, racines, 'json', environnement);
|
|
417
|
+
if (!graphe.stdout) return graphe.status || 1;
|
|
418
|
+
dossier = mkdtempSync(path.join(tmpdir(), 'frontieres-'));
|
|
419
|
+
const fichier = path.join(dossier, 'juges.txt');
|
|
420
|
+
writeFileSync(fichier, motifDes(aJuger(JSON.parse(graphe.stdout), sources)));
|
|
421
|
+
environnement[JUGES] = fichier;
|
|
422
|
+
}
|
|
423
|
+
const r = depcruise(racine, racines, 'json', environnement);
|
|
424
|
+
if (!r.stdout) return r.status || 1;
|
|
425
|
+
const resultat = JSON.parse(r.stdout);
|
|
426
|
+
const { refus, lignes } = trancher(racine, 'frontieres', constats(resultat));
|
|
427
|
+
// The report is dependency-cruiser's own (`err-long`): the result formatted as it is when the
|
|
428
|
+
// debt covers nothing, else a cruise that ignores the violations the debt covers, given as its
|
|
429
|
+
// known violations (`--ignore-known`).
|
|
430
|
+
const refusees = new Set(refus.map(c => c.violation));
|
|
431
|
+
const couvertes = resultat.summary.violations.filter(
|
|
432
|
+
v => v.rule.severity === 'error' && !refusees.has(v)
|
|
433
|
+
);
|
|
434
|
+
dossier ??= mkdtempSync(path.join(tmpdir(), 'frontieres-'));
|
|
435
|
+
if (couvertes.length) {
|
|
436
|
+
const connues = path.join(dossier, 'connues.json');
|
|
437
|
+
writeFileSync(connues, JSON.stringify(couvertes));
|
|
438
|
+
depcruise(racine, racines, 'err-long', environnement, connues);
|
|
439
|
+
} else {
|
|
440
|
+
const fichier = path.join(dossier, 'resultat.json');
|
|
441
|
+
writeFileSync(fichier, r.stdout);
|
|
442
|
+
spawnSync('npx', ['--no-install', 'depcruise-fmt', fichier, '--output-type', 'err-long'], {
|
|
443
|
+
cwd: racine,
|
|
444
|
+
stdio: 'inherit',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
for (const l of lignes) process.stderr.write(`${l}\n`);
|
|
448
|
+
return refus.length ? 1 : 0;
|
|
449
|
+
} finally {
|
|
450
|
+
if (dossier) rmSync(dossier, { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
455
|
+
process.exitCode = frontieres(process.cwd(), process.argv.slice(2));
|
|
456
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `grillhouse:gardes`: the frame's guards, the one list of them, played in its order.
|
|
3
|
+
//
|
|
4
|
+
// npx grillhouse gardes-du-cadre (npm run grillhouse:gardes, a task of Turborepo)
|
|
5
|
+
//
|
|
6
|
+
// - Each guard is a script of the package, beside this one, and plays in the working directory, under the environment of the run
|
|
7
|
+
// (`GRILLHOUSE_DETTE` among it, scripts/dette.mjs), whatever the verdict of the ones before: a
|
|
8
|
+
// red does not stop the pass, as `--continue` does for Turborepo's tasks.
|
|
9
|
+
// - Each red guard is named with its code at the end; the exit code is 1 when one guard is red,
|
|
10
|
+
// 0 when all are green.
|
|
11
|
+
// - While the run names its results directory (`GRILLHOUSE_RESULTATS`, scripts/resultats.mjs),
|
|
12
|
+
// each guard writes its findings there in SARIF; a red guard that wrote no finding has one
|
|
13
|
+
// written for it, without location, its name (its script's, without extension) and its code:
|
|
14
|
+
// a red no reader can place.
|
|
15
|
+
// - The build is not among them: Turborepo builds before this task (turbo.json).
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
+
import { ecrireSarif, lireSarif, RESULTATS } from './resultats.mjs';
|
|
20
|
+
|
|
21
|
+
// A script of the package, beside this one, by its name.
|
|
22
|
+
const ici = nom => fileURLToPath(new URL(nom, import.meta.url));
|
|
23
|
+
|
|
24
|
+
// The guards of the frame, each its script and its arguments, in the order they play.
|
|
25
|
+
export const GARDES_DU_CADRE = [
|
|
26
|
+
[ici('turbo-des-paquets.mjs')],
|
|
27
|
+
[ici('index-interfaces.mjs'), '--verifier'],
|
|
28
|
+
[ici('consommateurs.mjs')],
|
|
29
|
+
[ici('consommateurs-arbitres.mjs')],
|
|
30
|
+
[ici('frontieres.mjs')],
|
|
31
|
+
[ici('interfaces-contre-code.mjs')],
|
|
32
|
+
[ici('resumes.mjs')],
|
|
33
|
+
[ici('code-mort.mjs')],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// The name of each guard of the frame, as its findings carry it: its script's, without extension.
|
|
37
|
+
const nomDeGarde = garde => path.basename(garde[0]).replace(/\.[cm]?js$/, '');
|
|
38
|
+
export const NOMS_DES_GARDES = GARDES_DU_CADRE.map(nomDeGarde);
|
|
39
|
+
// A guard as its lines name it: its subcommand and its arguments.
|
|
40
|
+
const libelle = garde => ['grillhouse', nomDeGarde(garde), ...garde.slice(1)].join(' ');
|
|
41
|
+
|
|
42
|
+
// Plays each guard of <gardes> in <racine> under <env>; its verdict, 0 or 1. `dire` writes a line.
|
|
43
|
+
export function jouerLesGardes({
|
|
44
|
+
racine = process.cwd(),
|
|
45
|
+
gardes = GARDES_DU_CADRE,
|
|
46
|
+
env = process.env,
|
|
47
|
+
dire = l => process.stdout.write(`${l}\n`),
|
|
48
|
+
}) {
|
|
49
|
+
const rouges = [];
|
|
50
|
+
for (const garde of gardes) {
|
|
51
|
+
const avant = env[RESULTATS] ? lireSarif(env[RESULTATS]).constats.length : 0;
|
|
52
|
+
const r = spawnSync(process.execPath, garde, { cwd: racine, env, stdio: 'inherit' });
|
|
53
|
+
if (r.status === 0) continue;
|
|
54
|
+
const code = `code ${r.status ?? r.signal}`;
|
|
55
|
+
rouges.push(`${libelle(garde)} (${code})`);
|
|
56
|
+
if (env[RESULTATS] && lireSarif(env[RESULTATS]).constats.length === avant) {
|
|
57
|
+
ecrireSarif({
|
|
58
|
+
garde: nomDeGarde(garde),
|
|
59
|
+
racine,
|
|
60
|
+
env,
|
|
61
|
+
constats: [{ message: `${libelle(garde)} rouge (${code}), sans constat situé.` }],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (rouges.length === 0) return 0;
|
|
66
|
+
for (const r of rouges) dire(`⛔ garde rouge : ${r}`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
71
|
+
process.exitCode = jouerLesGardes({});
|
|
72
|
+
}
|