208 lines
5.9 KiB
JavaScript
Executable file
208 lines
5.9 KiB
JavaScript
Executable file
#!/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);
|
|
});
|