ktx/scripts/precommit-check.mjs

189 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

2026-05-10 23:12:26 +02:00
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
2026-05-12 13:02:06 +02:00
import { dirname, join, resolve } from 'node:path';
2026-05-10 23:12:26 +02:00
import { fileURLToPath } from 'node:url';
const scriptPath = fileURLToPath(import.meta.url);
2026-05-10 23:51:24 +02:00
const ktxRoot = dirname(dirname(scriptPath));
2026-05-10 23:12:26 +02:00
const packageNameByDir = new Map(
[
'cli',
'connector-bigquery',
'connector-clickhouse',
'connector-mysql',
'connector-postgres',
'connector-snowflake',
'connector-sqlite',
'connector-sqlserver',
'context',
'llm',
].map((packageDir) => {
2026-05-10 23:51:24 +02:00
const manifestPath = join(ktxRoot, 'packages', packageDir, 'package.json');
2026-05-10 23:12:26 +02:00
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
return [packageDir, manifest.name];
}),
);
const packageCodePattern = /\.(?:ts|tsx|js|jsx|json)$/;
const scriptPattern = /\.(?:mjs|js|json)$/;
const pythonPackageTests = new Map([
2026-05-10 23:51:24 +02:00
['ktx-sl', 'python/ktx-sl/tests'],
['ktx-daemon', 'python/ktx-daemon/tests'],
2026-05-10 23:12:26 +02:00
]);
function normalizeFilePath(filePath) {
2026-05-12 13:02:06 +02:00
const normalized = filePath.replaceAll('\\', '/').replace(/^\.\//, '');
return normalized.startsWith('ktx/') ? normalized.slice('ktx/'.length) : normalized;
2026-05-10 23:12:26 +02:00
}
function stablePush(commands, key, cmd, args) {
if (commands.some((command) => command.key === key)) {
return;
}
commands.push({ key, cmd, args });
}
function maybeScriptTest(scriptFile) {
if (scriptFile.endsWith('.test.mjs')) {
return scriptFile;
}
if (!scriptFile.endsWith('.mjs')) {
return null;
}
const testFile = scriptFile.replace(/\.mjs$/, '.test.mjs');
2026-05-10 23:51:24 +02:00
return existsSync(join(ktxRoot, testFile)) ? testFile : null;
2026-05-10 23:12:26 +02:00
}
export function planChecks(files) {
const commands = [];
const packageNames = new Set();
const pythonPackages = new Set();
let runBoundaryCheck = false;
let runAllTypeChecks = false;
let runAllPythonTests = false;
for (const rawFile of files) {
2026-05-12 13:02:06 +02:00
const ktxFile = normalizeFilePath(rawFile);
2026-05-10 23:12:26 +02:00
2026-05-10 23:51:24 +02:00
if (ktxFile.startsWith('packages/')) {
const [, packageDir, ...rest] = ktxFile.split('/');
2026-05-10 23:12:26 +02:00
const packageName = packageNameByDir.get(packageDir);
const packageFile = rest.join('/');
if (packageName && packageCodePattern.test(packageFile)) {
packageNames.add(packageName);
runBoundaryCheck = true;
}
continue;
}
2026-05-10 23:51:24 +02:00
if (ktxFile.startsWith('scripts/') && scriptPattern.test(ktxFile)) {
const testFile = maybeScriptTest(ktxFile);
2026-05-10 23:12:26 +02:00
if (testFile) {
stablePush(commands, `script-test:${testFile}`, 'node', ['--test', testFile]);
}
continue;
}
2026-05-10 23:51:24 +02:00
if (ktxFile.startsWith('python/')) {
const [, packageDir] = ktxFile.split('/');
2026-05-10 23:12:26 +02:00
if (pythonPackageTests.has(packageDir)) {
pythonPackages.add(packageDir);
}
continue;
}
if (
['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'release-policy.json', 'tsconfig.base.json'].includes(
2026-05-10 23:51:24 +02:00
ktxFile,
2026-05-10 23:12:26 +02:00
)
) {
runBoundaryCheck = true;
runAllTypeChecks = true;
continue;
}
2026-05-10 23:51:24 +02:00
if (['pyproject.toml', 'uv.lock', 'uv.toml'].includes(ktxFile)) {
2026-05-10 23:12:26 +02:00
runAllPythonTests = true;
}
}
if (runBoundaryCheck) {
stablePush(commands, 'boundary-check', 'node', ['scripts/check-boundaries.mjs']);
}
if (runAllTypeChecks) {
stablePush(commands, 'type-check:all', 'pnpm', ['--filter', './packages/*', 'run', 'type-check']);
} else {
for (const packageName of [...packageNames].sort()) {
stablePush(commands, `type-check:${packageName}`, 'pnpm', ['--filter', packageName, 'run', 'type-check']);
stablePush(commands, `build:${packageName}`, 'pnpm', ['--filter', `${packageName}...`, 'run', 'build']);
stablePush(commands, `test:${packageName}`, 'pnpm', ['--filter', packageName, 'run', 'test']);
}
}
if (runAllPythonTests) {
stablePush(commands, 'pytest:all', 'uv', ['run', 'pytest']);
} else {
for (const packageDir of [...pythonPackages].sort()) {
stablePush(commands, `pytest:${packageDir}`, 'uv', [
'run',
'--package',
packageDir,
'pytest',
pythonPackageTests.get(packageDir),
]);
}
}
return commands;
}
function printCommand(command) {
console.log(`\n$ ${command.cmd} ${command.args.join(' ')}`);
}
export function runChecks(files) {
const commands = planChecks(files);
if (commands.length === 0) {
2026-05-10 23:51:24 +02:00
console.log('No KTX package checks needed for these files.');
2026-05-10 23:12:26 +02:00
return 0;
}
for (const command of commands) {
printCommand(command);
const result = spawnSync(command.cmd, command.args, {
2026-05-10 23:51:24 +02:00
cwd: ktxRoot,
2026-05-10 23:12:26 +02:00
stdio: 'inherit',
env: process.env,
});
if (result.error) {
console.error(result.error.message);
return 1;
}
if (result.status !== 0) {
return result.status ?? 1;
}
}
return 0;
}
2026-05-12 13:02:06 +02:00
if (process.argv[1] && resolve(process.argv[1]) === scriptPath) {
2026-05-10 23:12:26 +02:00
process.exitCode = runChecks(process.argv.slice(2));
}