@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,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The write lock of a role agent: a PreToolUse hook declared in the settings of its session
|
|
3
|
+
// (scripts/droits.mjs: `npx grillhouse verrou <role>`). It reads the hook's JSON on stdin and refuses a
|
|
4
|
+
// file write the role does not own, with exit code 2 and its reason on stderr.
|
|
5
|
+
//
|
|
6
|
+
// - A path under the system temp directory (the session scratchpad) is always writable.
|
|
7
|
+
// - testwriter writes the files it owns (`isTesterFile`): test files, and every file of a package
|
|
8
|
+
// of the shared test material.
|
|
9
|
+
// - The repository root is that of the git repository holding the written file, whatever the
|
|
10
|
+
// agent's working directory; a file outside any repository is read against the working directory.
|
|
11
|
+
// - developpeur writes none of the testwriter's files, and no other file until its ticket's
|
|
12
|
+
// description holds a "## Architecture" section. The ticket is the "TON TICKET : <id>" line of the agent's
|
|
13
|
+
// launch prompt, read from its transcript.
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
import { matieres, NOM_DE_TEST } from '../consommateurs.mjs';
|
|
20
|
+
|
|
21
|
+
const WRITE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit', 'NotebookEdit']);
|
|
22
|
+
|
|
23
|
+
export function isTestFile(file) {
|
|
24
|
+
const parts = file.split(/[\\/]/);
|
|
25
|
+
const base = parts[parts.length - 1];
|
|
26
|
+
return (
|
|
27
|
+
parts.slice(0, -1).some(d => d === 'test' || d === 'tests' || d === '__tests__') ||
|
|
28
|
+
NOM_DE_TEST.test(base)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// True when <file>, absolute or relative to the repository root <racine>, is written by the
|
|
33
|
+
// testwriter: a test file, or a file of a package of the shared test material (a component of
|
|
34
|
+
// <racine> whose package exports `./test-fixtures`, `matieres` of `scripts/consommateurs.mjs`).
|
|
35
|
+
// The one rule of ownership, read by the locks of both roles and by the integration's lots.
|
|
36
|
+
export function isTesterFile(file, racine) {
|
|
37
|
+
const abs = path.resolve(racine, file);
|
|
38
|
+
if (isTestFile(path.relative(path.resolve(racine), abs))) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return matieres(racine).some(m => isInside(abs, m.dir));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// True when <file> lies strictly under the directory <dir>.
|
|
45
|
+
function isInside(file, dir) {
|
|
46
|
+
const rel = path.relative(path.resolve(dir), file);
|
|
47
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isScratch = (file, tmp = tmpdir()) => isInside(file, tmp);
|
|
51
|
+
|
|
52
|
+
// The root of the git repository (or worktree) that holds the absolute path <file>, read from its
|
|
53
|
+
// nearest existing directory; null when no repository holds it.
|
|
54
|
+
function racineDe(file) {
|
|
55
|
+
let dir = path.dirname(file);
|
|
56
|
+
while (!existsSync(dir)) {
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) return null;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
stdio: 'pipe',
|
|
65
|
+
}).trim();
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The ticket id of the launch prompt. A sub-agent's prompt opens its own transcript, next to the
|
|
72
|
+
// session's; an agent launched as its own session (`claude --agent`) has it in the session's.
|
|
73
|
+
function ticketOf(input) {
|
|
74
|
+
if (!input.transcript_path) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const transcript = input.agent_id
|
|
78
|
+
? path.join(
|
|
79
|
+
input.transcript_path.replace(/\.jsonl$/, ''),
|
|
80
|
+
'subagents',
|
|
81
|
+
`agent-${input.agent_id}.jsonl`
|
|
82
|
+
)
|
|
83
|
+
: input.transcript_path;
|
|
84
|
+
let first;
|
|
85
|
+
try {
|
|
86
|
+
first = readFileSync(transcript, 'utf8')
|
|
87
|
+
.split('\n')
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map(l => JSON.parse(l))
|
|
90
|
+
.find(e => e.type === 'user');
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const content = first?.message?.content;
|
|
95
|
+
const text = Array.isArray(content) ? content.map(c => c.text ?? '').join('\n') : content;
|
|
96
|
+
const m = /TON TICKET\s*:\s*([A-Za-z0-9_.-]+)/.exec(text ?? '');
|
|
97
|
+
return m ? m[1] : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ticketDescription(id, cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const out = execFileSync('bd', ['show', id, '--json'], {
|
|
103
|
+
cwd,
|
|
104
|
+
encoding: 'utf8',
|
|
105
|
+
stdio: 'pipe',
|
|
106
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
107
|
+
});
|
|
108
|
+
return JSON.parse(out)[0]?.description ?? null;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The refusal reason for this hook input, or null when the write is allowed.
|
|
115
|
+
export function refusal(role, input, { tmp = tmpdir(), describe = ticketDescription } = {}) {
|
|
116
|
+
if (!WRITE_TOOLS.has(input.tool_name)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const raw = input.tool_input?.file_path ?? input.tool_input?.notebook_path;
|
|
120
|
+
if (!raw) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const cwd = input.cwd ?? process.cwd();
|
|
124
|
+
const file = path.resolve(cwd, raw);
|
|
125
|
+
if (isScratch(file, tmp)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const racine = racineDe(file) ?? cwd;
|
|
129
|
+
const testwriter = isTesterFile(file, racine);
|
|
130
|
+
|
|
131
|
+
if (role === 'testwriter') {
|
|
132
|
+
return testwriter
|
|
133
|
+
? null
|
|
134
|
+
: `Le testwriter écrit seulement des fichiers de test et la matière de test partagée ; ${raw} n'est ni l'un ni l'autre. Le code revient au développeur.`;
|
|
135
|
+
}
|
|
136
|
+
if (role === 'developpeur') {
|
|
137
|
+
if (testwriter) {
|
|
138
|
+
return isTestFile(path.relative(path.resolve(racine), file))
|
|
139
|
+
? `Les tests appartiennent au testwriter : ${raw} est un fichier de test. Un test faux se signale au superviseur.`
|
|
140
|
+
: `La matière de test partagée appartient au testwriter : ${raw} en fait partie. Un besoin de matière se signale au superviseur.`;
|
|
141
|
+
}
|
|
142
|
+
const id = ticketOf(input);
|
|
143
|
+
if (!id) {
|
|
144
|
+
return 'Ton ticket est introuvable : ton message de lancement porte « TON TICKET : <id> ».';
|
|
145
|
+
}
|
|
146
|
+
const description = describe(id, input.cwd);
|
|
147
|
+
if (description === null) {
|
|
148
|
+
return `Le ticket ${id} est illisible par bd.`;
|
|
149
|
+
}
|
|
150
|
+
if (!/^##\s+Architecture\b/m.test(description)) {
|
|
151
|
+
return `Le ticket ${id} n'a pas sa section « ## Architecture » : le code attend l'architecture. Rends la question au superviseur.`;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return `Rôle inconnu du verrou : ${role}.`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
159
|
+
const input = JSON.parse(readFileSync(0, 'utf8'));
|
|
160
|
+
const reason = refusal(process.argv[2], input);
|
|
161
|
+
if (reason) {
|
|
162
|
+
process.stderr.write(reason + '\n');
|
|
163
|
+
process.exitCode = 2;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The progress of a pass, as Bazel counts its actions « [1 234 / 2 000] »: the variable that names
|
|
2
|
+
// the progress directory of a pass, `GRILLHOUSE_AVANCEMENT`, the reporter the base configuration
|
|
3
|
+
// adds while it is set (./base.mjs, ./avancement.reporter.mjs), and the writing of a record. The
|
|
4
|
+
// reading, the plan and the totals kept are scripts/avancement.mjs.
|
|
5
|
+
//
|
|
6
|
+
// - Each task of a pass writes its own record to `<dossier>/taches/<nom>.json`: `{ hash, faits,
|
|
7
|
+
// prevus, a }`, the hash of its Turborepo task (`TURBO_HASH`, null out of Turborepo), its files
|
|
8
|
+
// done and planned, and the instant of the writing (ms). Two packages, or the same one twice,
|
|
9
|
+
// never write the same file.
|
|
10
|
+
// - A record is written whole or not at all: to a file of its own, renamed over the record.
|
|
11
|
+
// - The variable passes through Turborepo without entering any hash (`globalPassThroughEnv` of the
|
|
12
|
+
// root's turbo.json, which scripts/turbo-des-paquets.mjs writes and checks).
|
|
13
|
+
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
export const AVANCEMENT = 'GRILLHOUSE_AVANCEMENT';
|
|
18
|
+
// The subdirectory of the records in a progress directory.
|
|
19
|
+
export const TACHES = 'taches';
|
|
20
|
+
|
|
21
|
+
// Writes <objet> as JSON to <fichier>, whole or not at all.
|
|
22
|
+
export function ecrireAtomique(fichier, objet) {
|
|
23
|
+
mkdirSync(path.dirname(fichier), { recursive: true });
|
|
24
|
+
const provisoire = `${fichier}.${process.pid}.tmp`;
|
|
25
|
+
writeFileSync(provisoire, JSON.stringify(objet));
|
|
26
|
+
renameSync(provisoire, fichier);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Writes the record <nom> of the progress directory <dossier>: <faits> done of <prevus>, now.
|
|
30
|
+
export function ecrireTache(dossier, nom, { hash = null, faits, prevus }) {
|
|
31
|
+
ecrireAtomique(path.join(dossier, TACHES, `${nom}.json`), { hash, faits, prevus, a: Date.now() });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The reporter of progress under <env>'s progress directory, as Vitest's `reporters` take it; an
|
|
35
|
+
// empty list without the variable.
|
|
36
|
+
export function rapportAvancement(env = process.env) {
|
|
37
|
+
if (!env[AVANCEMENT]) return [];
|
|
38
|
+
return [fileURLToPath(new URL('./avancement.reporter.mjs', import.meta.url))];
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The reporter of progress of the base configuration, while the variable GRILLHOUSE_AVANCEMENT
|
|
2
|
+
// names the progress directory of a pass (./avancement.mjs): it writes the record of its task
|
|
3
|
+
// once Vitest knows the files it plays (their number, the files planned), then at each test file
|
|
4
|
+
// done, then at the end of the run, every file done. The name of the record is made of the package
|
|
5
|
+
// directory it runs in and its process; it carries the hash of its Turborepo task (TURBO_HASH), by
|
|
6
|
+
// which the pass knows it again in Turborepo's cache (scripts/avancement.mjs).
|
|
7
|
+
import { AVANCEMENT, ecrireTache } from './avancement.mjs';
|
|
8
|
+
|
|
9
|
+
const FINIS = new Set(['pass', 'fail', 'skip', 'todo']);
|
|
10
|
+
|
|
11
|
+
export default class AvancementReporter {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.dossier = process.env[AVANCEMENT];
|
|
14
|
+
this.nom = `${process.cwd().split('/').filter(Boolean).join('-') || 'racine'}-${process.pid}`;
|
|
15
|
+
this.prevus = 0;
|
|
16
|
+
this.faits = new Set();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
onInit(ctx) {
|
|
20
|
+
this.ctx = ctx;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ecrire() {
|
|
24
|
+
try {
|
|
25
|
+
ecrireTache(this.dossier, this.nom, {
|
|
26
|
+
hash: process.env.TURBO_HASH ?? null,
|
|
27
|
+
faits: this.faits.size,
|
|
28
|
+
prevus: Math.max(this.prevus, this.faits.size),
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
// A directory that does not write leaves the run going, without its progress.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onPathsCollected(fichiers = []) {
|
|
36
|
+
this.prevus = fichiers.length;
|
|
37
|
+
this.ecrire();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onTaskUpdate(paquets = []) {
|
|
41
|
+
let change = false;
|
|
42
|
+
for (const [id, resultat] of paquets) {
|
|
43
|
+
const t = this.ctx?.state?.idMap?.get(id);
|
|
44
|
+
if (!t?.filepath || t.file !== t || !FINIS.has(resultat?.state)) continue;
|
|
45
|
+
if (this.faits.has(id)) continue;
|
|
46
|
+
this.faits.add(id);
|
|
47
|
+
change = true;
|
|
48
|
+
}
|
|
49
|
+
if (change) this.ecrire();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onFinished(fichiers = []) {
|
|
53
|
+
for (const f of fichiers) this.faits.add(f.id);
|
|
54
|
+
this.ecrire();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The base of a pass: the temporary directory of a run, its own and removed at its end, as Bazel
|
|
2
|
+
// gives each test its temporary directory (TEST_TMPDIR) under the base of its run. A pass killed
|
|
3
|
+
// leaves its base, which the next pass sweeps, as Bazel cleans its sandboxes.
|
|
4
|
+
//
|
|
5
|
+
// - A base is a directory `<dans>/grillhouse-<genre>-XXXXXX/`, `<dans>` the temporary directory of
|
|
6
|
+
// whoever opens it (`os.tmpdir()`). It holds its owner (`.grillhouse-base`: the process that
|
|
7
|
+
// opened it and that process's PID namespace) and `t/`, the directory where the pass and its
|
|
8
|
+
// processes write: the pass names it in `TMPDIR` to its children, so `os.tmpdir()`, `mktemp` and
|
|
9
|
+
// Claude Code (`CLAUDE_CODE_TMPDIR`, else `os.tmpdir()`) write there.
|
|
10
|
+
// - The genres: `vitest`, each run of Vitest (`baseDeVitest`, a plugin of the base configuration,
|
|
11
|
+
// ./base.mjs), which holds Vitest's own temporary directory too; `tests`, `npm test` (scripts/tests.mjs); `verification`,
|
|
12
|
+
// `npm run verifier` (scripts/verifier.mjs), whose base Turborepo passes to its tasks
|
|
13
|
+
// (`globalPassThroughEnv`, scripts/turbo-des-paquets.mjs); `circuit`, the results and progress
|
|
14
|
+
// of the passes of a circuit's run (scripts/circuit.mjs; its working directory is the ticket's,
|
|
15
|
+
// beside its copy, which no sweep touches); `integration`, the working directory of an
|
|
16
|
+
// integration (scripts/integration/integrer.mjs); `enveloppe`, the throwaway state of a session's envelope
|
|
17
|
+
// (scripts/enveloppe/enveloppe.mjs).
|
|
18
|
+
// - The base is removed at the end of the pass, green or red (`fermer`).
|
|
19
|
+
// - Opening a base first sweeps `<dans>`: every base there whose owner is gone is removed, whatever
|
|
20
|
+
// its genre. A base is judged by its owner's process only when that owner lives in the PID
|
|
21
|
+
// namespace of the sweeper (an envelope has its own, bwrap --unshare-pid, where a number means
|
|
22
|
+
// nothing outside): a base of another namespace, a base without a readable owner (one being
|
|
23
|
+
// created) and a base whose owner lives are never touched.
|
|
24
|
+
import {
|
|
25
|
+
mkdirSync,
|
|
26
|
+
mkdtempSync,
|
|
27
|
+
readdirSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
readlinkSync,
|
|
30
|
+
rmSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
} from 'node:fs';
|
|
33
|
+
import { tmpdir } from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
|
|
36
|
+
// The prefix of a base's name, and the file of its owner.
|
|
37
|
+
const PREFIXE = 'grillhouse-';
|
|
38
|
+
const PROPRIETAIRE = '.grillhouse-base';
|
|
39
|
+
|
|
40
|
+
/** Whether the process `pid` exists. */
|
|
41
|
+
export function vivant(pid) {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return e.code === 'EPERM';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The PID namespace of this process, or null when /proc does not tell it.
|
|
51
|
+
function espace() {
|
|
52
|
+
try {
|
|
53
|
+
return readlinkSync('/proc/self/ns/pid');
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Whether the base <base> is an orphan: its owner reads, lives in this namespace, and is gone.
|
|
60
|
+
function orpheline(base) {
|
|
61
|
+
let p;
|
|
62
|
+
try {
|
|
63
|
+
p = JSON.parse(readFileSync(path.join(base, PROPRIETAIRE), 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const ici = espace();
|
|
68
|
+
return ici !== null && p.espace === ici && Number.isInteger(p.pid) && !vivant(p.pid);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Removes every orphan base in <dans>.
|
|
72
|
+
function balayer(dans) {
|
|
73
|
+
let noms;
|
|
74
|
+
try {
|
|
75
|
+
noms = readdirSync(dans);
|
|
76
|
+
} catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const nom of noms) {
|
|
80
|
+
const base = path.join(dans, nom);
|
|
81
|
+
if (nom.startsWith(PREFIXE) && orpheline(base)) rmSync(base, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Opens the base of a pass of <genre> in <dans>, after sweeping the orphan bases there.
|
|
87
|
+
* @param {string} genre what the pass is (`vitest`, `circuit`…), in the base's name
|
|
88
|
+
* @param {{ dans?: string }} options `dans`, the directory of the base (`os.tmpdir()`)
|
|
89
|
+
* @returns {{ base: string, dossier: string, fermer: () => void }} the base, the directory its
|
|
90
|
+
* pass writes in (`TMPDIR` of its children), and the removal of the base
|
|
91
|
+
*/
|
|
92
|
+
export function ouvrirBase(genre, { dans = tmpdir() } = {}) {
|
|
93
|
+
balayer(dans);
|
|
94
|
+
mkdirSync(dans, { recursive: true });
|
|
95
|
+
const base = mkdtempSync(path.join(dans, `${PREFIXE}${genre}-`));
|
|
96
|
+
writeFileSync(
|
|
97
|
+
path.join(base, PROPRIETAIRE),
|
|
98
|
+
`${JSON.stringify({ pid: process.pid, espace: espace() })}\n`
|
|
99
|
+
);
|
|
100
|
+
const dossier = path.join(base, 't');
|
|
101
|
+
mkdirSync(dossier);
|
|
102
|
+
return { base, dossier, fermer: () => rmSync(base, { recursive: true, force: true }) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The variable that marks the base of a run of Vitest: `<process>:<base>`, the process that opened
|
|
106
|
+
// it. A configuration loaded again in that process (Vite bundles each load) keeps that base.
|
|
107
|
+
const MARQUE = 'GRILLHOUSE_BASE_DE_VITEST';
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The plugin of the base configuration that gives a run of Vitest its base: while Vite resolves
|
|
111
|
+
* the configuration, before Vitest makes its own temporary directory (`join(tmpdir(), nanoid())`,
|
|
112
|
+
* its transformed modules), the main process opens a base of genre `vitest` and names it in its
|
|
113
|
+
* `TMPDIR`, which every test process inherits; the base is removed when the process exits, green
|
|
114
|
+
* or red. A process that already opened its base keeps it.
|
|
115
|
+
* @returns {{ name: string, config: () => void }} a plugin of Vite
|
|
116
|
+
*/
|
|
117
|
+
export function baseDeVitest() {
|
|
118
|
+
return {
|
|
119
|
+
name: 'grillhouse:base-de-passe',
|
|
120
|
+
config() {
|
|
121
|
+
if (process.env[MARQUE]?.startsWith(`${process.pid}:`)) return;
|
|
122
|
+
const b = ouvrirBase('vitest');
|
|
123
|
+
process.env[MARQUE] = `${process.pid}:${b.base}`;
|
|
124
|
+
process.env.TMPDIR = b.dossier;
|
|
125
|
+
process.on('exit', b.fermer);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The types of ./base.mjs, for a Vitest configuration written in TypeScript that imports
|
|
2
|
+
// `@roomi-fields/grillhouse/vitest-config` and extends it (`mergeConfig`).
|
|
3
|
+
import type { UserConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
declare const base: UserConfig;
|
|
6
|
+
export default base;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The frame's base configuration of the day's tests, exported by the frame's package as
|
|
2
|
+
// `@roomi-fields/grillhouse/vitest-config`: every Vitest configuration of a project extends it
|
|
3
|
+
// (`mergeConfig(base, defineConfig({ … }))`) and adds its own `include`, coverage and options
|
|
4
|
+
// without repeating what this file sets.
|
|
5
|
+
//
|
|
6
|
+
// - Every configuration imports it by its name (`import base from
|
|
7
|
+
// '@roomi-fields/grillhouse/vitest-config'`), the root's of a project as each package's, as the
|
|
8
|
+
// examples of Turborepo reach their configuration packages (@repo/eslint-config): a package is
|
|
9
|
+
// reached by its name, never by a path into it. In a repository of packages, each tested
|
|
10
|
+
// package declares the frame's package in its devDependencies, which the update of the frame
|
|
11
|
+
// writes (scripts/grillhouse-maj.mjs). Grillhouse's package imports it by its own name.
|
|
12
|
+
//
|
|
13
|
+
// - The exclusions of the day: a night test (`*.nuit.test.*`, `*.nuit.spec.*`) is too long for the
|
|
14
|
+
// day, and the measures of a time budget bench (`*.budget.test.*`, `*.budget.spec.*`) move with
|
|
15
|
+
// the load of the day: `npm test`, `npm run verifier` and the integration leave both out, and
|
|
16
|
+
// the night plays them (scripts/nuit.sh, scripts/vitest.nuit.config.mjs); a targeted run that
|
|
17
|
+
// names the file of one itself plays it, under the measure of time below (./cibles.mjs, where
|
|
18
|
+
// the patterns live). `mergeConfig` adds the exclusions of a configuration that extends this one
|
|
19
|
+
// to these.
|
|
20
|
+
// - A run that plays no test fails, as Vitest's default has it (`passWithNoTests` unset): a
|
|
21
|
+
// targeted run (`npm test -- <paths>`, scripts/tests.mjs) plays each path in its package only,
|
|
22
|
+
// and a path that names no test is a mistake, never a green.
|
|
23
|
+
// - The tests a targeted run names arrive in the variable `GRILLHOUSE_TESTS`, which a plugin
|
|
24
|
+
// reads once the configuration is merged and turns into the files played (./cibles.mjs); without
|
|
25
|
+
// it, nothing changes. The same plugin restricts a pass measured against the last green to the
|
|
26
|
+
// tests Vitest relates to the files changed since it (`GRILLHOUSE_CHANGEMENTS`,
|
|
27
|
+
// ./changements.mjs). `mergeConfig` adds the plugins of a configuration that extends this one to
|
|
28
|
+
// it.
|
|
29
|
+
// - The common environment: Node, no watch, and git's settings for the tests by the environment
|
|
30
|
+
// (GIT_CONFIG_COUNT/KEY/VALUE, git-config(1)), the automatic maintenance and garbage collection
|
|
31
|
+
// off (maintenance.auto=false, gc.auto=0). This file is the one address of these settings; the
|
|
32
|
+
// night configuration takes them from here.
|
|
33
|
+
// - A time witness is measured with the CPU at rest: a setup file notes the CPU pressure at the
|
|
34
|
+
// start and at the end of each test (./mesure.setup.mjs), and the duration reporter, beside
|
|
35
|
+
// Vitest's default one, gives the exit code 75, « à repasser », when every failure depends on a
|
|
36
|
+
// duration and one took place under load (./duree.reporter.mjs; the signal and its threshold,
|
|
37
|
+
// ./pression.mjs). A test that measures time marks itself (`mesurerAuRepos`,
|
|
38
|
+
// `@roomi-fields/grillhouse/vitest-config/mesure`). `mergeConfig` adds the reporters and the setup files of
|
|
39
|
+
// a configuration that extends this one to these; a reporter named on the command line
|
|
40
|
+
// (`--reporter`) replaces them, and Vitest's code then stands.
|
|
41
|
+
// - The machine of a test file is its own: a setup file gives it a lock of the heavy measure of
|
|
42
|
+
// its own, so that a `npm run verifier` a test plays in a repository of trial neither waits for
|
|
43
|
+
// the lock of the machine nor holds it (./machine.setup.mjs, scripts/verrou-de-machine.mjs).
|
|
44
|
+
// - The time witnesses play alone in `npm run verifier`: the plugin below leaves them out of its
|
|
45
|
+
// pass, then plays them in a pass of their own (./cibles.mjs, scripts/verifier.mjs).
|
|
46
|
+
// - While a run names its results directory (`GRILLHOUSE_RESULTATS`, ./resultats.mjs), the JSON
|
|
47
|
+
// reporter writes Vitest's report there too, which the circuit reads (scripts/resultats.mjs); a
|
|
48
|
+
// setup file gives each test file's processes a results directory of their own, so that a run a
|
|
49
|
+
// test plays in a repository of trial never writes in it (./resultats.setup.mjs).
|
|
50
|
+
// - A run of Vitest has a base of its own, as Bazel gives each test its TEST_TMPDIR: a plugin opens
|
|
51
|
+
// it while the configuration resolves, sweeping the orphan bases a killed run left, and names
|
|
52
|
+
// it in TMPDIR, so that Vitest's own temporary directory, each test process and everything they
|
|
53
|
+
// start write there; the base is removed when the run exits, green or red
|
|
54
|
+
// (./base-de-passe.mjs).
|
|
55
|
+
// - While a run names the progress directory of its pass (`GRILLHOUSE_AVANCEMENT`,
|
|
56
|
+
// ./avancement.mjs), the reporter of progress writes there the files done and planned of its
|
|
57
|
+
// task at each test file done, which the board reads (scripts/avancement.mjs,
|
|
58
|
+
// scripts/tableau.mjs).
|
|
59
|
+
import { fileURLToPath } from 'node:url';
|
|
60
|
+
import { baseDeVitest } from './base-de-passe.mjs';
|
|
61
|
+
import { ciblage, HORS_DU_JOUR } from './cibles.mjs';
|
|
62
|
+
import { rapportAvancement } from './avancement.mjs';
|
|
63
|
+
import { rapportJson } from './resultats.mjs';
|
|
64
|
+
|
|
65
|
+
const ici = f => fileURLToPath(new URL(f, import.meta.url));
|
|
66
|
+
|
|
67
|
+
export default {
|
|
68
|
+
plugins: [baseDeVitest(), ciblage()],
|
|
69
|
+
test: {
|
|
70
|
+
environment: 'node',
|
|
71
|
+
env: {
|
|
72
|
+
GIT_CONFIG_COUNT: '2',
|
|
73
|
+
GIT_CONFIG_KEY_0: 'maintenance.auto',
|
|
74
|
+
GIT_CONFIG_VALUE_0: 'false',
|
|
75
|
+
GIT_CONFIG_KEY_1: 'gc.auto',
|
|
76
|
+
GIT_CONFIG_VALUE_1: '0',
|
|
77
|
+
},
|
|
78
|
+
exclude: ['**/node_modules/**', '**/dist/**', ...HORS_DU_JOUR],
|
|
79
|
+
reporters: ['default', ici('./duree.reporter.mjs'), ...rapportJson(), ...rapportAvancement()],
|
|
80
|
+
setupFiles: [
|
|
81
|
+
ici('./mesure.setup.mjs'),
|
|
82
|
+
ici('./machine.setup.mjs'),
|
|
83
|
+
ici('./resultats.setup.mjs'),
|
|
84
|
+
],
|
|
85
|
+
watch: false,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The changes since the last green that a pass measures itself against (scripts/dernier-vert.mjs
|
|
2
|
+
// writes them), as the plugin of ./cibles.mjs reads them in the package whose configuration it
|
|
3
|
+
// runs: the tests Vitest relates to them, or every test of the package when one of them lies
|
|
4
|
+
// where Vitest's graph of a test does not reach. `vitest --changed <ref>` is the model: Vitest
|
|
5
|
+
// relates a test to the files it reads from git (`<ref>...HEAD`, the files staged, modified and
|
|
6
|
+
// untracked), and plays the tests whose graph holds one of them (`related`); the frame gives it
|
|
7
|
+
// the files that differ from the tree of the last green instead, which Vitest's git cannot read
|
|
8
|
+
// (a green kept on a copy whose lot is not committed).
|
|
9
|
+
//
|
|
10
|
+
// - The variable `GRILLHOUSE_CHANGEMENTS` names a JSON file, `{ racine, fichiers, supprimes }`: the
|
|
11
|
+
// top of the repository, the files changed or added since the last green and the files removed,
|
|
12
|
+
// each path from the top. It passes through Turborepo without entering any hash (the root's
|
|
13
|
+
// `globalPassThroughEnv`, which scripts/turbo-des-paquets.mjs writes and checks): a package whose
|
|
14
|
+
// inputs did not move replays its last green from Turborepo's cache, whatever the last green.
|
|
15
|
+
// The plugin takes it out of the environment once read: no process the tests start receives it.
|
|
16
|
+
// - The graph of a test is the test file and the modules it imports, statically or dynamically,
|
|
17
|
+
// node_modules left out (`getTestDependencies` of Vitest). Vitest does not see what lies out of
|
|
18
|
+
// it, and the package plays whole (`entier`) when a change lies there, the reason written:
|
|
19
|
+
// 1. a file removed, in the package or in a package it depends on: no graph of today holds it;
|
|
20
|
+
// 2. the frame's base configuration (this directory), which every configuration extends;
|
|
21
|
+
// 3. the configuration of the repository at its top: `package.json`, `package-lock.json`,
|
|
22
|
+
// `turbo.json`, `turbo.jsonc`, `tsconfig*.json`;
|
|
23
|
+
// 4. the configuration of the package at its top: `package.json`, `vitest.config.*`,
|
|
24
|
+
// `vite.config.*`, `vitest.workspace.*`, `tsconfig*.json`; its `setupFiles` and its
|
|
25
|
+
// `globalSetup`, which Vitest runs beside the graph;
|
|
26
|
+
// 5. a path its tests declare they read (`grillhouse.lit` of its package.json, from the top):
|
|
27
|
+
// a script they run, a file they read, as Bazel's `data`;
|
|
28
|
+
// 6. a file of a package it depends on (its `dependencies`, `devDependencies`,
|
|
29
|
+
// `optionalDependencies` and `peerDependencies` that resolve to a directory of the repository,
|
|
30
|
+
// out of node_modules, transitively), a test file of that package aside: its tests read that
|
|
31
|
+
// package built, not the source git follows;
|
|
32
|
+
// 7. a file of the package that no graph can hold, its extension not that of a module (`.js`,
|
|
33
|
+
// `.mjs`, `.cjs`, `.ts`, `.mts`, `.cts`, `.jsx`, `.tsx`, `.json`): data, a template, a
|
|
34
|
+
// script of the shell, a document, which a test reads without importing it.
|
|
35
|
+
// Vitest's own `forceRerunTriggers` (a configuration's, or its default) still apply, as with
|
|
36
|
+
// `--changed`.
|
|
37
|
+
// - Otherwise the tests played are those Vitest relates to the files changed or added (`related`,
|
|
38
|
+
// absolute), and the files a targeted run names themselves (./cibles.mjs) join them: a file named
|
|
39
|
+
// plays, changed or not. A package where none relates plays none and passes
|
|
40
|
+
// (`passWithNoTests`), as Vitest's `--changed` has it.
|
|
41
|
+
//
|
|
42
|
+
// This module uses Node alone.
|
|
43
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
46
|
+
|
|
47
|
+
export const CHANGEMENTS = 'GRILLHOUSE_CHANGEMENTS';
|
|
48
|
+
// This directory, the frame's base configuration.
|
|
49
|
+
const BASE = path.dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
// The extension of a module, which a graph of Vitest can hold.
|
|
51
|
+
const MODULE = /\.(?:[cm]?[jt]sx?|json)$/;
|
|
52
|
+
// A test file, as Vitest's default `include` names one.
|
|
53
|
+
const TEST = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
54
|
+
const DU_DEPOT = /^(?:package\.json|package-lock\.json|turbo\.jsonc?|tsconfig[^/]*\.json)$/;
|
|
55
|
+
const DU_PAQUET =
|
|
56
|
+
/^(?:package\.json|(?:vitest|vite)\.config\.[^/]+|vitest\.workspace\.[^/]+|tsconfig[^/]*\.json)$/;
|
|
57
|
+
const CHAMPS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
|
|
58
|
+
|
|
59
|
+
const lireJson = f => {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(readFileSync(f, 'utf8'));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Whether the absolute path <f> lies under the directory <dir> (or is it).
|
|
68
|
+
const sous = (f, dir) => {
|
|
69
|
+
const r = path.relative(dir, f);
|
|
70
|
+
return r === '' || (!r.startsWith('..') && !path.isAbsolute(r));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// The changes the file at <chemin> holds, `{ racine, fichiers, supprimes }`; null when it does not
|
|
74
|
+
// read.
|
|
75
|
+
export function lireChangements(chemin) {
|
|
76
|
+
const c = lireJson(chemin);
|
|
77
|
+
const liste = l => Array.isArray(l) && l.every(x => typeof x === 'string');
|
|
78
|
+
if (!c || typeof c.racine !== 'string' || !liste(c.fichiers) || !liste(c.supprimes)) return null;
|
|
79
|
+
return c;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The directories of the packages of the repository at <racine> that the package at <dossier>
|
|
83
|
+
// depends on, transitively, itself left out.
|
|
84
|
+
function dependances(racine, dossier) {
|
|
85
|
+
const vus = new Set([dossier]);
|
|
86
|
+
const out = [];
|
|
87
|
+
const suivre = d => {
|
|
88
|
+
const pkg = lireJson(path.join(d, 'package.json')) ?? {};
|
|
89
|
+
const noms = new Set(CHAMPS.flatMap(c => Object.keys(pkg[c] ?? {})));
|
|
90
|
+
for (const nom of noms) {
|
|
91
|
+
for (let ici = d; sous(ici, racine); ici = path.dirname(ici)) {
|
|
92
|
+
const lien = path.join(ici, 'node_modules', nom);
|
|
93
|
+
if (!existsSync(lien)) {
|
|
94
|
+
if (ici === racine) break;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let reel;
|
|
98
|
+
try {
|
|
99
|
+
reel = realpathSync(lien);
|
|
100
|
+
} catch {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const r = path.relative(racine, reel);
|
|
104
|
+
if (sous(reel, racine) && !r.split(path.sep).includes('node_modules') && !vus.has(reel)) {
|
|
105
|
+
vus.add(reel);
|
|
106
|
+
out.push(reel);
|
|
107
|
+
suivre(reel);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
suivre(dossier);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Why the package whose configuration's root is <dossier> plays whole under the changes <ch>, its
|
|
118
|
+
// test configuration <test>: `{ raison, fichier }`, the first change out of Vitest's graph (the
|
|
119
|
+
// path from the top of the repository); null when Vitest sees them all.
|
|
120
|
+
function paquetEntier(ch, dossier, test = {}) {
|
|
121
|
+
const racine = ch.racine;
|
|
122
|
+
const abs = f => path.join(racine, f);
|
|
123
|
+
const deps = dependances(racine, dossier);
|
|
124
|
+
const lit = (lireJson(path.join(dossier, 'package.json'))?.grillhouse?.lit ?? [])
|
|
125
|
+
.filter(l => typeof l === 'string')
|
|
126
|
+
.map(l => l.replace(/^\.\//, '').replace(/\/+$/, ''));
|
|
127
|
+
const preparation = [test.setupFiles, test.globalSetup]
|
|
128
|
+
.flat()
|
|
129
|
+
.filter(f => typeof f === 'string')
|
|
130
|
+
.map(f => path.resolve(dossier, f));
|
|
131
|
+
for (const f of ch.supprimes) {
|
|
132
|
+
if ([dossier, ...deps].some(d => sous(abs(f), d))) {
|
|
133
|
+
return { raison: 'un fichier supprimé, que le graphe des tests ne tient plus', fichier: f };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const f of ch.fichiers) {
|
|
137
|
+
const a = abs(f);
|
|
138
|
+
const raison = sous(a, BASE)
|
|
139
|
+
? 'la configuration de base du cadre'
|
|
140
|
+
: !f.includes('/') && DU_DEPOT.test(f)
|
|
141
|
+
? 'la configuration du dépôt'
|
|
142
|
+
: path.dirname(a) === dossier && DU_PAQUET.test(path.basename(a))
|
|
143
|
+
? 'la configuration du paquet'
|
|
144
|
+
: preparation.includes(a)
|
|
145
|
+
? 'un fichier de préparation des tests'
|
|
146
|
+
: lit.some(l => f === l || f.startsWith(`${l}/`))
|
|
147
|
+
? 'un chemin que ses tests déclarent lire (grillhouse.lit)'
|
|
148
|
+
: deps.some(d => sous(a, d)) && !TEST.test(f)
|
|
149
|
+
? 'le code d’un paquet dont il dépend, que ses tests lisent construit'
|
|
150
|
+
: sous(a, dossier) && !MODULE.test(f)
|
|
151
|
+
? 'un fichier qu’aucun module n’importe, qu’un test lit sans l’importer'
|
|
152
|
+
: null;
|
|
153
|
+
if (raison) return { raison, fichier: f };
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Restricts the test configuration <test> of the package at <dossier> to the changes <ch>: the
|
|
159
|
+
// tests Vitest relates to them and the files <nommes> (absolute), or every test, when a change
|
|
160
|
+
// lies out of Vitest's graph; <dire> writes the line that says which.
|
|
161
|
+
export function restreindre(test, dossier, ch, nommes, dire) {
|
|
162
|
+
const nom = path.relative(ch.racine, dossier) || '.';
|
|
163
|
+
const entier = paquetEntier(ch, dossier, test);
|
|
164
|
+
if (entier) {
|
|
165
|
+
dire(`↳ ${nom} : tous ses tests, ${entier.raison} (${entier.fichier}).`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
test.related = [...new Set([...ch.fichiers.map(f => path.join(ch.racine, f)), ...nommes])];
|
|
169
|
+
test.passWithNoTests = true;
|
|
170
|
+
dire(
|
|
171
|
+
`↳ ${nom} : les tests que Vitest relie aux ${ch.fichiers.length} fichier(s) changé(s) depuis le dernier vert.`
|
|
172
|
+
);
|
|
173
|
+
}
|