#!/usr/bin/env node /** * Porte de qualité — exécute les vérifications EN PARALLÈLE. * * Les cinq vérifications sont indépendantes (aucune ne lit la sortie d'une * autre). Les enchaîner avec `&&` imposait deux coûts inutiles : * - la somme des durées au lieu du maximum ; * - sept invocations `npm run` imbriquées, chacune avec son propre coût de * démarrage npm (~300 ms). * * On les lance donc toutes d'un coup, on tamponne les sorties, et on les * restitue dans un ordre stable à la fin. Le détail par tâche reste affiché * pour que le goulot d'étranglement demeure visible. * * `--sequential` rétablit l'exécution en série (utile pour isoler un échec * quand deux tâches écrivent des messages entremêlés). */ import { spawn } from "node:child_process"; const isWin = process.platform === "win32"; const sequential = process.argv.includes("--sequential"); /** bin = binaire de node_modules/.bin (résolu par npm via PATH). */ const TASKS = [ { name: "eslint", cmd: "eslint", args: [".", "--cache", "--cache-location", "node_modules/.cache/eslint/"], }, { name: "tsc", cmd: "tsc", args: ["-p", "jsconfig.json"], }, // Séparer les tests API (node) et frontend (jsdom) en deux processus a été // mesuré : aucun gain (4,9 s dans les deux cas — c'est la mise en place de // jsdom qui domine, et les deux processus se disputent les cœurs). Gardé en // un seul. { name: "vitest", cmd: "vitest", args: ["run"] }, { name: "ruff", cmd: "python", args: ["-m", "ruff", "check", "."], }, // Les deux suites Python tournent dans des processus distincts : chaque app a // son propre paquet `src`, et les réunir dans un seul pytest fait que le // premier `src` importé masque l'autre (ModuleNotFoundError: src.parser). // Séparées, elles s'exécutent aussi en parallèle. { name: "pytest:geocoder", cmd: "python", args: ["-m", "pytest", "tests", "-q"], cwd: "apps/geocoder" }, { name: "pytest:crawler", cmd: "python", args: ["-m", "pytest", "tests", "-q"], cwd: "apps/crawler" }, ]; function run(task) { return new Promise((resolve) => { const started = Date.now(); const child = spawn(task.cmd, task.args, { shell: isWin, cwd: task.cwd ?? process.cwd(), }); let out = ""; child.stdout.on("data", (b) => (out += b)); child.stderr.on("data", (b) => (out += b)); child.on("error", (err) => { resolve({ ...task, code: 1, out: `${err.message}\n`, ms: Date.now() - started }); }); child.on("close", (code) => { resolve({ ...task, code, out, ms: Date.now() - started }); }); }); } const started = Date.now(); let results; if (sequential) { results = []; for (const t of TASKS) results.push(await run(t)); } else { results = await Promise.all(TASKS.map(run)); } const failed = results.filter((r) => r.code !== 0); for (const r of results) { const mark = r.code === 0 ? "✓" : "✗"; console.log(`\n${mark} ${r.name} (${(r.ms / 1000).toFixed(1)}s)`); // Sur succès on ne montre que les avertissements éventuels, pas le bruit. if (r.code !== 0 || /warning|warn/i.test(r.out)) { const body = r.out.trimEnd(); if (body) console.log(body.replace(/^/gm, " ")); } } const total = ((Date.now() - started) / 1000).toFixed(1); if (failed.length) { console.error(`\n✗ Porte de qualité en échec : ${failed.map((f) => f.name).join(", ")} (${total}s)`); process.exit(1); } console.log(`\n✓ Tout est vert (${total}s)`);