#!/usr/bin/env node /** * API Changes Check Script * * Checks for unintended API changes and blocks release if API changed * without proper commit type (feat/breaking). * * @author Matthew Raymer * @version 1.0.0 */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const API_CHECKSUM_FILE = path.join(__dirname, '..', 'api-checksum.txt'); const DIST_TYPES_DIR = path.join(__dirname, '..', 'dist', 'esm'); /** * Generate checksum of API definitions * @returns {string} Checksum of API files */ function generateApiChecksum() { try { // Get all .d.ts files in dist/esm const typeFiles = execSync(`find "${DIST_TYPES_DIR}" -name "*.d.ts" -type f`, { encoding: 'utf8' }) .trim() .split('\n') .filter(file => file.length > 0); if (typeFiles.length === 0) { console.warn('āš ļø No TypeScript definition files found in dist/esm'); return ''; } // Generate checksum of all type files const checksum = execSync(`cat ${typeFiles.join(' ')} | shasum -a 256`, { encoding: 'utf8' }) .trim() .split(' ')[0]; return checksum; } catch (error) { console.error('Error generating API checksum:', error.message); return ''; } } /** * Get last commit message * @returns {string} Last commit message */ function getLastCommitMessage() { try { return execSync('git log -1 --pretty=%B', { encoding: 'utf8' }).trim(); } catch (error) { console.error('Error getting last commit message:', error.message); return ''; } } /** * Check if commit message indicates API change * @param {string} commitMessage - Commit message * @returns {boolean} True if API change is allowed */ function isApiChangeAllowed(commitMessage) { const allowedPatterns = [ /^feat\(/i, /^fix\(/i, /BREAKING CHANGE/i, /^feat!/i, /^fix!/i ]; return allowedPatterns.some(pattern => pattern.test(commitMessage)); } /** * Check API changes and enforce rules */ function checkApiChanges() { console.log('šŸ” Checking API changes...'); if (!fs.existsSync(DIST_TYPES_DIR)) { console.error('āŒ Dist types directory not found. Run "npm run build" first.'); process.exit(1); } const currentChecksum = generateApiChecksum(); if (!currentChecksum) { console.error('āŒ Could not generate API checksum'); process.exit(1); } let previousChecksum = ''; if (fs.existsSync(API_CHECKSUM_FILE)) { previousChecksum = fs.readFileSync(API_CHECKSUM_FILE, 'utf8').trim(); } if (previousChecksum === currentChecksum) { console.log('āœ… No API changes detected'); return; } console.log('āš ļø API changes detected'); console.log(`Previous: ${previousChecksum.substring(0, 8)}...`); console.log(`Current: ${currentChecksum.substring(0, 8)}...`); const commitMessage = getLastCommitMessage(); console.log(`Last commit: ${commitMessage}`); if (!isApiChangeAllowed(commitMessage)) { console.error('\nāŒ API changes detected without proper commit type!'); console.error('🚫 Release blocked. API changes require:'); console.error(' - feat(scope): description'); console.error(' - fix(scope): description'); console.error(' - BREAKING CHANGE in commit message'); console.error(' - feat!(scope): or fix!(scope): for breaking changes'); process.exit(1); } console.log('āœ… API changes allowed by commit type'); // Update checksum file fs.writeFileSync(API_CHECKSUM_FILE, currentChecksum); console.log('šŸ“ Updated API checksum file'); } // Run the check checkApiChanges();