Skip to content

Commit e05fcbf

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 67967c7 commit e05fcbf

File tree

4 files changed

+620
-0
lines changed

4 files changed

+620
-0
lines changed

scripts/check.mjs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,69 @@ async function main() {
284284
}
285285
}
286286

287+
// Run markdown filenames validation check
288+
if (runAll) {
289+
if (!quiet) {
290+
logger.progress('Validating markdown filenames')
291+
}
292+
exitCode = await runCommandQuiet('node', [
293+
'scripts/validate-markdown-filenames.mjs',
294+
]).then(r => r.exitCode)
295+
if (exitCode !== 0) {
296+
if (!quiet) {
297+
logger.error('Markdown filenames validation failed')
298+
}
299+
process.exitCode = exitCode
300+
return
301+
}
302+
if (!quiet) {
303+
logger.clearLine().done('Markdown filenames validated')
304+
logger.error('')
305+
}
306+
}
307+
308+
// Run file size validation check
309+
if (runAll) {
310+
if (!quiet) {
311+
logger.progress('Validating file sizes')
312+
}
313+
exitCode = await runCommandQuiet('node', [
314+
'scripts/validate-file-size.mjs',
315+
]).then(r => r.exitCode)
316+
if (exitCode !== 0) {
317+
if (!quiet) {
318+
logger.error('File size validation failed')
319+
}
320+
process.exitCode = exitCode
321+
return
322+
}
323+
if (!quiet) {
324+
logger.clearLine().done('File sizes validated')
325+
logger.error('')
326+
}
327+
}
328+
329+
// Run file count validation check
330+
if (runAll) {
331+
if (!quiet) {
332+
logger.progress('Validating file count')
333+
}
334+
exitCode = await runCommandQuiet('node', [
335+
'scripts/validate-file-count.mjs',
336+
]).then(r => r.exitCode)
337+
if (exitCode !== 0) {
338+
if (!quiet) {
339+
logger.error('File count validation failed')
340+
}
341+
process.exitCode = exitCode
342+
return
343+
}
344+
if (!quiet) {
345+
logger.clearLine().done('File count validated')
346+
logger.error('')
347+
}
348+
}
349+
287350
if (!quiet) {
288351
logger.success('All checks passed')
289352
printFooter()

scripts/validate-file-count.mjs

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

scripts/validate-file-size.mjs

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

0 commit comments

Comments
 (0)