Next 16 mit App Router und standalone-Ausgabe, React 19, TypeScript strict, Tailwind 4, Vitest und Playwright. Alle Versionen exakt gepinnt. Abweichungen vom ursprünglich notierten Stand, jeweils begründet: - Prisma 7.10.0 statt 6, weil 6 überholt ist und 8 nur als RC vorliegt. - TypeScript 6.0.3 statt 7, weil typescript-eslint 8 nur "<6.1.0" trägt. - ESLint 9.39.5 statt 10, weil das in eslint-config-next gebündelte eslint-plugin-react noch context.getFilename() benutzt. - npm-overrides für SimpleWebAuthn und nodemailer, weil next-auth dort ältere optionale Peers deklariert. Die betroffenen next-auth-Provider werden nicht benutzt: Passkeys laufen direkt über SimpleWebAuthn 14. "npm run pruefen" bündelt Prettier, ESLint, tsc, Vitest und ShellCheck. ShellCheck läuft über das Docker-Abbild, wenn es lokal fehlt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
146 lines
4.1 KiB
JavaScript
146 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Sammelpruefung vor jedem Commit: Format, Lint, Typen, Tests, Shellskripte.
|
|
* Bricht beim ersten Fehler ab und nennt den Schritt, der gefallen ist.
|
|
*
|
|
* Die Werkzeuge werden ueber ihren JavaScript-Einstiegspunkt gestartet, nicht
|
|
* ueber npx. Das spart eine Shell und laeuft unter Windows wie unter Linux
|
|
* gleich.
|
|
*/
|
|
import { spawnSync } from 'node:child_process'
|
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
import { join, relative, sep } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const WURZEL = fileURLToPath(new URL('..', import.meta.url))
|
|
const istWindows = process.platform === 'win32'
|
|
|
|
/** Windows-Pfad in Schraegstrich-Schreibweise, wie Docker sie erwartet. */
|
|
function mitSchraegstrich(pfad) {
|
|
return pfad.split(sep).join('/')
|
|
}
|
|
|
|
/** Ein mitgeliefertes Werkzeug im selben Node starten. */
|
|
function werkzeug(paket, einstieg, argumente) {
|
|
const ergebnis = spawnSync(
|
|
process.execPath,
|
|
[join(WURZEL, 'node_modules', paket, einstieg), ...argumente],
|
|
{ cwd: WURZEL, stdio: 'inherit' },
|
|
)
|
|
return ergebnis.status ?? 1
|
|
}
|
|
|
|
function programm(befehl, argumente, optionen = {}) {
|
|
const ergebnis = spawnSync(befehl, argumente, {
|
|
cwd: WURZEL,
|
|
stdio: 'inherit',
|
|
...optionen,
|
|
})
|
|
return ergebnis.status ?? 1
|
|
}
|
|
|
|
function vorhanden(befehl) {
|
|
const ergebnis = spawnSync(istWindows ? 'where' : 'which', [befehl], {
|
|
cwd: WURZEL,
|
|
stdio: 'ignore',
|
|
})
|
|
return ergebnis.status === 0
|
|
}
|
|
|
|
const UEBERGEHEN = new Set([
|
|
'node_modules',
|
|
'.git',
|
|
'.next',
|
|
'coverage',
|
|
'logs',
|
|
'sicherungen',
|
|
'playwright-report',
|
|
'test-results',
|
|
])
|
|
|
|
/** Alle .sh-Dateien im Projekt, ohne node_modules und Konsorten. */
|
|
function shellskripteFinden(verzeichnis = WURZEL, gefunden = []) {
|
|
for (const eintrag of readdirSync(verzeichnis)) {
|
|
if (UEBERGEHEN.has(eintrag)) continue
|
|
const pfad = join(verzeichnis, eintrag)
|
|
if (statSync(pfad).isDirectory()) {
|
|
shellskripteFinden(pfad, gefunden)
|
|
} else if (eintrag.endsWith('.sh')) {
|
|
gefunden.push(mitSchraegstrich(relative(WURZEL, pfad)))
|
|
}
|
|
}
|
|
return gefunden
|
|
}
|
|
|
|
function shellskriptePruefen() {
|
|
const skripte = shellskripteFinden()
|
|
if (skripte.length === 0) {
|
|
console.log(' Keine Shellskripte gefunden.')
|
|
return 0
|
|
}
|
|
console.log(` ${skripte.length} Skript(e): ${skripte.join(', ')}`)
|
|
const argumente = ['--shell=bash', '--severity=style', ...skripte]
|
|
|
|
if (vorhanden('shellcheck')) {
|
|
return programm('shellcheck', argumente)
|
|
}
|
|
|
|
if (programm('docker', ['info'], { stdio: 'ignore' }) === 0) {
|
|
console.log(' shellcheck ist nicht installiert, nehme das Docker-Abbild.')
|
|
return programm('docker', [
|
|
'run',
|
|
'--rm',
|
|
'-v',
|
|
`${mitSchraegstrich(WURZEL)}:/mnt`,
|
|
'koalaman/shellcheck:stable',
|
|
...argumente,
|
|
])
|
|
}
|
|
|
|
console.error(
|
|
'Fehler: Weder shellcheck noch ein laufender Docker-Daemon gefunden. ' +
|
|
'Entweder shellcheck installieren (https://www.shellcheck.net/) oder ' +
|
|
'Docker starten. Die Shellskripte bleiben sonst ungeprueft.',
|
|
)
|
|
return 1
|
|
}
|
|
|
|
const schritte = [
|
|
{
|
|
name: 'Format (Prettier)',
|
|
tun: () => werkzeug('prettier', 'bin/prettier.cjs', ['--check', '.']),
|
|
},
|
|
{
|
|
name: 'Lint (ESLint)',
|
|
tun: () => werkzeug('eslint', 'bin/eslint.js', ['.', '--max-warnings', '0']),
|
|
},
|
|
{
|
|
name: 'Typen (TypeScript)',
|
|
tun: () => werkzeug('typescript', 'bin/tsc', ['--noEmit']),
|
|
},
|
|
{ name: 'Tests (Vitest)', tun: () => werkzeug('vitest', 'vitest.mjs', ['run']) },
|
|
{ name: 'Shellskripte (ShellCheck)', tun: shellskriptePruefen },
|
|
]
|
|
|
|
// Ohne erzeugten Prisma-Client schlagen Typen und Tests fehl.
|
|
if (!existsSync(join(WURZEL, 'src', 'generated', 'prisma', 'client.ts'))) {
|
|
console.log('▸ Prisma-Client fehlt, erzeuge ihn')
|
|
if (werkzeug('prisma', 'build/index.js', ['generate']) !== 0) process.exit(1)
|
|
}
|
|
|
|
let gefallen = null
|
|
for (const schritt of schritte) {
|
|
console.log(`▸ ${schritt.name}`)
|
|
if (schritt.tun() !== 0) {
|
|
gefallen = schritt.name
|
|
break
|
|
}
|
|
}
|
|
|
|
if (gefallen) {
|
|
console.error(`✗ Gescheitert: ${gefallen}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('✓ Alle Pruefungen bestanden.')
|