Skip to content

Commit f42852e

Browse files
committed
feat(scripts): add file validation checks
- Add validate-file-size.mjs: checks files don't exceed 2MB - Add validate-file-count.mjs: ensures commits don't exceed 50 files - Add validate-markdown-filenames.mjs: enforces markdown naming conventions - Integrate all validators into check.mjs
1 parent 81f4365 commit f42852e

File tree

4 files changed

+574
-0
lines changed

4 files changed

+574
-0
lines changed

scripts/check.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ async function main() {
6363
...(process.platform === 'win32' && { shell: true }),
6464
},
6565
},
66+
{
67+
args: ['scripts/validate-markdown-filenames.mjs'],
68+
command: 'node',
69+
options: {
70+
...(process.platform === 'win32' && { shell: true }),
71+
},
72+
},
73+
{
74+
args: ['scripts/validate-file-size.mjs'],
75+
command: 'node',
76+
options: {
77+
...(process.platform === 'win32' && { shell: true }),
78+
},
79+
},
80+
{
81+
args: ['scripts/validate-file-count.mjs'],
82+
command: 'node',
83+
options: {
84+
...(process.platform === 'win32' && { shell: true }),
85+
},
86+
},
6687
]
6788

6889
const exitCodes = await runParallel(checks)

scripts/validate-file-count.mjs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env node
2+
/**
3+
* @fileoverview Validates that commits don't contain too many files.
4+
*
5+
* Rules:
6+
* - No single commit should contain 50+ files
7+
* - Helps catch accidentally staging too many files or generated content
8+
* - Prevents overly large commits that are hard to review
9+
*/
10+
11+
import { exec } from 'node:child_process';
12+
import path from 'node:path';
13+
import { promisify } from 'node:util';
14+
import { fileURLToPath } from 'node:url';
15+
import { logger } from './utils/logger.mjs';
16+
17+
const execAsync = promisify(exec);
18+
19+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
20+
const rootPath = path.join(__dirname, '..');
21+
22+
// Maximum number of files in a single commit
23+
const MAX_FILES_PER_COMMIT = 50;
24+
25+
/**
26+
* Check if too many files are staged for commit.
27+
*/
28+
async function validateStagedFileCount() {
29+
try {
30+
// Check if we're in a git repository
31+
const { stdout: gitRoot } = await execAsync('git rev-parse --show-toplevel', {
32+
cwd: rootPath,
33+
});
34+
35+
if (!gitRoot.trim()) {
36+
return null; // Not a git repository
37+
}
38+
39+
// Get list of staged files
40+
const { stdout } = await execAsync('git diff --cached --name-only', { cwd: rootPath });
41+
42+
const stagedFiles = stdout
43+
.trim()
44+
.split('\n')
45+
.filter(line => line.length > 0);
46+
47+
if (stagedFiles.length >= MAX_FILES_PER_COMMIT) {
48+
return {
49+
count: stagedFiles.length,
50+
files: stagedFiles,
51+
limit: MAX_FILES_PER_COMMIT,
52+
};
53+
}
54+
55+
return null;
56+
} catch {
57+
// Not a git repo or git not available
58+
return null;
59+
}
60+
}
61+
62+
async function main() {
63+
try {
64+
const violation = await validateStagedFileCount();
65+
66+
if (!violation) {
67+
logger.success('Commit size is acceptable');
68+
process.exitCode = 0;
69+
return;
70+
}
71+
72+
logger.fail('Too many files staged for commit');
73+
logger.log('');
74+
logger.log(`Staged files: ${violation.count}`);
75+
logger.log(`Maximum allowed: ${violation.limit}`);
76+
logger.log('');
77+
logger.log('Staged files:');
78+
logger.log('');
79+
80+
// Show first 20 files, then summary if more
81+
const filesToShow = violation.files.slice(0, 20);
82+
for (const file of filesToShow) {
83+
logger.log(` ${file}`);
84+
}
85+
86+
if (violation.files.length > 20) {
87+
logger.log(` ... and ${violation.files.length - 20} more files`);
88+
}
89+
90+
logger.log('');
91+
logger.log(
92+
'Split into smaller commits, check for accidentally staged files, or exclude generated files.',
93+
);
94+
logger.log('');
95+
96+
process.exitCode = 1;
97+
} catch (error) {
98+
logger.fail(`Validation failed: ${error.message}`);
99+
process.exitCode = 1;
100+
}
101+
}
102+
103+
main().catch(error => {
104+
logger.fail(`Validation failed: ${error}`);
105+
process.exitCode = 1;
106+
});

scripts/validate-file-size.mjs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env node
2+
/**
3+
* @fileoverview Validates that no individual files exceed size threshold.
4+
*
5+
* Rules:
6+
* - No single file should exceed 2MB (2,097,152 bytes)
7+
* - Helps prevent accidental commits of large binaries, data files, or artifacts
8+
* - Excludes: node_modules, .git, dist, build, coverage directories
9+
*/
10+
11+
import { promises as fs } from 'node:fs';
12+
import path from 'node:path';
13+
import { fileURLToPath } from 'node:url';
14+
import { logger } from './utils/logger.mjs';
15+
16+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
17+
const rootPath = path.join(__dirname, '..');
18+
19+
// Maximum file size: 2MB
20+
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2,097,152 bytes
21+
22+
// Directories to skip
23+
const SKIP_DIRS = new Set([
24+
'node_modules',
25+
'.git',
26+
'dist',
27+
'build',
28+
'.cache',
29+
'coverage',
30+
'.next',
31+
'.nuxt',
32+
'.output',
33+
'.turbo',
34+
'.vercel',
35+
'.vscode',
36+
'tmp',
37+
]);
38+
39+
/**
40+
* Format bytes to human-readable size.
41+
*/
42+
function formatBytes(bytes) {
43+
if (bytes === 0) return '0 B';
44+
const k = 1024;
45+
const sizes = ['B', 'KB', 'MB', 'GB'];
46+
const i = Math.floor(Math.log(bytes) / Math.log(k));
47+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
48+
}
49+
50+
/**
51+
* Recursively scan directory for files exceeding size limit.
52+
*/
53+
async function scanDirectory(dir, violations = []) {
54+
try {
55+
const entries = await fs.readdir(dir, { withFileTypes: true });
56+
57+
for (const entry of entries) {
58+
const fullPath = path.join(dir, entry.name);
59+
60+
if (entry.isDirectory()) {
61+
// Skip excluded directories and hidden directories (except .claude, .config, .github)
62+
if (
63+
!SKIP_DIRS.has(entry.name) &&
64+
(!entry.name.startsWith('.') ||
65+
entry.name === '.claude' ||
66+
entry.name === '.config' ||
67+
entry.name === '.github')
68+
) {
69+
await scanDirectory(fullPath, violations);
70+
}
71+
} else if (entry.isFile()) {
72+
try {
73+
const stats = await fs.stat(fullPath);
74+
if (stats.size > MAX_FILE_SIZE) {
75+
const relativePath = path.relative(rootPath, fullPath);
76+
violations.push({
77+
file: relativePath,
78+
size: stats.size,
79+
formattedSize: formatBytes(stats.size),
80+
maxSize: formatBytes(MAX_FILE_SIZE),
81+
});
82+
}
83+
} catch {
84+
// Skip files we can't stat
85+
}
86+
}
87+
}
88+
} catch {
89+
// Skip directories we can't read
90+
}
91+
92+
return violations;
93+
}
94+
95+
/**
96+
* Validate file sizes in repository.
97+
*/
98+
async function validateFileSizes() {
99+
const violations = await scanDirectory(rootPath);
100+
101+
// Sort by size descending (largest first)
102+
violations.sort((a, b) => b.size - a.size);
103+
104+
return violations;
105+
}
106+
107+
async function main() {
108+
try {
109+
const violations = await validateFileSizes();
110+
111+
if (violations.length === 0) {
112+
logger.success('All files are within size limits');
113+
process.exitCode = 0;
114+
return;
115+
}
116+
117+
logger.fail('File size violations found');
118+
logger.log('');
119+
logger.log(`Maximum allowed file size: ${formatBytes(MAX_FILE_SIZE)}`);
120+
logger.log('');
121+
logger.log('Files exceeding limit:');
122+
logger.log('');
123+
124+
for (const violation of violations) {
125+
logger.log(` ${violation.file}`);
126+
logger.log(` Size: ${violation.formattedSize}`);
127+
logger.log(` Exceeds limit by: ${formatBytes(violation.size - MAX_FILE_SIZE)}`);
128+
logger.log('');
129+
}
130+
131+
logger.log(
132+
'Reduce file sizes, move large files to external storage, or exclude from repository.',
133+
);
134+
logger.log('');
135+
136+
process.exitCode = 1;
137+
} catch (error) {
138+
logger.fail(`Validation failed: ${error.message}`);
139+
process.exitCode = 1;
140+
}
141+
}
142+
143+
main().catch(error => {
144+
logger.fail(`Validation failed: ${error}`);
145+
process.exitCode = 1;
146+
});

0 commit comments

Comments
 (0)