#!/usr/bin/env node /** * Validate Path Aliases - Precommit Hook * * Detects relative path aliases to @lilith/* packages in vite.config.* and tsconfig.json files. * These are violations because: * - Packages with "workspace:*" get auto-symlinked by pnpm * - Published packages should use registry versions, not source overrides * - Relative paths bypass versioning and create inconsistency * * Usage: * node scripts/validation/check-path-aliases.mjs [--staged] * * Options: * --staged Only check staged files (for pre-commit hook) * --all Check all files (default) * * Exit codes: * 0 - No violations found * 1 - Violations found */ import { execFile } from 'child_process'; import { promisify } from 'util'; import { readFileSync, existsSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const execFileAsync = promisify(execFile); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const CODEBASE_ROOT = resolve(__dirname, '../..'); // Parse CLI args const stagedOnly = process.argv.includes('--staged'); // Color codes const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const GREEN = '\x1b[32m'; const BLUE = '\x1b[34m'; const RESET = '\x1b[0m'; const BOLD = '\x1b[1m'; /** * Get list of files to check */ async function getFilesToCheck() { try { let output; if (stagedOnly) { // Get staged files const result = await execFileAsync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACM'], { cwd: CODEBASE_ROOT, encoding: 'utf8' }); output = result.stdout.trim(); } else { // Find all vite.config and tsconfig files const result = await execFileAsync('find', [ '.', '-type', 'f', '(', '-name', 'vite.config.ts', '-o', '-name', 'vite.config.js', '-o', '-name', 'tsconfig.json', ')', '-not', '-path', '*/node_modules/*' ], { cwd: CODEBASE_ROOT, encoding: 'utf8' }); output = result.stdout.trim(); } if (!output) return []; const files = output.split('\n').filter(f => { return f.match(/vite\.config\.(ts|js)$/) || f.match(/tsconfig\.json$/); }); return files; } catch (error) { if (stagedOnly && error.code === 128) { // Not a git repo or no staged files return []; } throw error; } } /** * Check a file for @lilith/* path alias violations */ function checkFile(filePath) { const fullPath = resolve(CODEBASE_ROOT, filePath); if (!existsSync(fullPath)) { return []; } const content = readFileSync(fullPath, 'utf8'); const violations = []; // Pattern 1: Vite path.resolve aliases // '@lilith/design-tokens': path.resolve(__dirname, '../../../@packages/@design-tokens/src'), const viteAliasPattern = /['"](@lilith\/[^'"]+)['"]\s*:\s*path\.resolve\([^)]+@packages/g; // Pattern 2: TypeScript path mappings // "@lilith/design-tokens": ["../../../@packages/@design-tokens/src"] const tsconfigPathPattern = /"(@lilith\/[^"]+)"\s*:\s*\[[^\]]*@packages/g; // Pattern 3: Catch any @lilith/* with path.resolve or relative path const genericPattern = /['"](@lilith\/[^'"]+)['"]\s*:\s*(?:path\.resolve|["'][.\/])/g; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNum = i + 1; // Check all patterns let match; const patterns = [viteAliasPattern, tsconfigPathPattern, genericPattern]; for (const pattern of patterns) { pattern.lastIndex = 0; // Reset regex while ((match = pattern.exec(line)) !== null) { const packageName = match[1]; violations.push({ file: filePath, line: lineNum, package: packageName, content: line.trim() }); } } } return violations; } /** * Main validation */ async function main() { console.log(`${BLUE}${BOLD}🔍 Checking for @lilith/* path alias violations...${RESET}\n`); const files = await getFilesToCheck(); if (files.length === 0) { console.log(`${GREEN}✓ No files to check${RESET}`); process.exit(0); } console.log(`${BLUE}Scanning ${files.length} file(s)...${RESET}\n`); let totalViolations = 0; const violationsByFile = new Map(); for (const file of files) { const violations = checkFile(file); if (violations.length > 0) { violationsByFile.set(file, violations); totalViolations += violations.length; } } if (totalViolations === 0) { console.log(`${GREEN}${BOLD}✓ No path alias violations found!${RESET}\n`); process.exit(0); } // Report violations console.log(`${RED}${BOLD}✗ Found ${totalViolations} path alias violation(s):${RESET}\n`); for (const [file, violations] of violationsByFile.entries()) { console.log(`${RED}${file}${RESET}`); for (const violation of violations) { console.log(` ${YELLOW}Line ${violation.line}:${RESET} ${violation.package}`); console.log(` ${violation.content.substring(0, 100)}${violation.content.length > 100 ? '...' : ''}`); } console.log(''); } console.log(`${YELLOW}${BOLD}Why this is a violation:${RESET}`); console.log(` • Packages with "workspace:*" get auto-symlinked by pnpm`); console.log(` • Published packages should use registry versions`); console.log(` • Relative paths bypass versioning and create inconsistency\n`); console.log(`${BLUE}${BOLD}To fix:${RESET}`); console.log(` 1. Remove the path alias from the file`); console.log(` 2. Ensure the package is in package.json dependencies`); console.log(` 3. For workspace packages, use "workspace:*"`); console.log(` 4. For published packages, use version like "^1.0.0"\n`); console.log(`${BLUE}${BOLD}To skip this check:${RESET}`); console.log(` git commit --no-verify\n`); process.exit(1); } main().catch(error => { console.error(`${RED}Error:${RESET}`, error.message); process.exit(1); });