project-structure-lint 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.validate-structurerc.example.json +45 -0
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +142 -0
- package/LICENSE +21 -0
- package/PROJECT_SUMMARY.md +252 -0
- package/QUICK_START.md +164 -0
- package/README.md +330 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +121 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +71 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/core/validator.d.ts +16 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +231 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/index.d.ts +5 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +17 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/presets/react.d.ts +3 -0
- package/dist/presets/react.d.ts.map +1 -0
- package/dist/presets/react.js +94 -0
- package/dist/presets/react.js.map +1 -0
- package/dist/reporters/consoleReporter.d.ts +9 -0
- package/dist/reporters/consoleReporter.d.ts.map +1 -0
- package/dist/reporters/consoleReporter.js +98 -0
- package/dist/reporters/consoleReporter.js.map +1 -0
- package/dist/types/index.d.ts +59 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/fileScanner.d.ts +20 -0
- package/dist/utils/fileScanner.d.ts.map +1 -0
- package/dist/utils/fileScanner.js +166 -0
- package/dist/utils/fileScanner.js.map +1 -0
- package/dist/utils/naming.d.ts +4 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +75 -0
- package/dist/utils/naming.js.map +1 -0
- package/jest.config.js +17 -0
- package/package.json +48 -0
- package/src/cli.ts +106 -0
- package/src/config/loader.ts +79 -0
- package/src/core/validator.ts +242 -0
- package/src/index.ts +6 -0
- package/src/presets/index.ts +16 -0
- package/src/presets/react.ts +93 -0
- package/src/reporters/consoleReporter.ts +116 -0
- package/src/types/index.ts +67 -0
- package/src/types/micromatch.d.ts +41 -0
- package/src/utils/__tests__/naming.test.ts +107 -0
- package/src/utils/fileScanner.ts +162 -0
- package/src/utils/naming.ts +99 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import micromatch from 'micromatch';
|
|
3
|
+
import { ProjectConfig, ValidationError, ValidationResult, FilePattern } from '../types';
|
|
4
|
+
import { FileScanner, ScannedFile } from '../utils/fileScanner';
|
|
5
|
+
import { validateNamingConvention, getExpectedNaming } from '../utils/naming';
|
|
6
|
+
|
|
7
|
+
export class ProjectValidator {
|
|
8
|
+
private config: ProjectConfig;
|
|
9
|
+
private scanner: FileScanner;
|
|
10
|
+
private errors: ValidationError[] = [];
|
|
11
|
+
private warnings: ValidationError[] = [];
|
|
12
|
+
|
|
13
|
+
constructor(config: ProjectConfig, rootDir: string = process.cwd()) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
const ignorePatterns = config.ignore || [];
|
|
16
|
+
this.scanner = new FileScanner(rootDir, ignorePatterns);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async validate(): Promise<ValidationResult> {
|
|
20
|
+
this.errors = [];
|
|
21
|
+
this.warnings = [];
|
|
22
|
+
|
|
23
|
+
const files = await this.scanner.scanDirectory();
|
|
24
|
+
const filesScanned = files.filter(f => !f.isDirectory).length;
|
|
25
|
+
const directoriesScanned = files.filter(f => f.isDirectory).length;
|
|
26
|
+
|
|
27
|
+
// Validate component co-location
|
|
28
|
+
if (this.config.rules?.componentColocation?.enabled) {
|
|
29
|
+
await this.validateComponentColocation();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate file naming conventions
|
|
33
|
+
if (this.config.rules?.fileNaming) {
|
|
34
|
+
await this.validateFileNaming(files);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate folder structure
|
|
38
|
+
if (this.config.rules?.folderStructure) {
|
|
39
|
+
await this.validateFolderStructure(files);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
valid: this.errors.length === 0,
|
|
44
|
+
errors: this.errors,
|
|
45
|
+
warnings: this.warnings,
|
|
46
|
+
filesScanned,
|
|
47
|
+
directoriesScanned
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async validateComponentColocation(): Promise<void> {
|
|
52
|
+
const colocationConfig = this.config.rules?.componentColocation;
|
|
53
|
+
if (!colocationConfig) return;
|
|
54
|
+
|
|
55
|
+
const componentDirs = this.scanner.getComponentDirectories(
|
|
56
|
+
colocationConfig.componentDirs
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
for (const dir of componentDirs) {
|
|
60
|
+
const contents = await this.scanner.getDirectoryContents(dir);
|
|
61
|
+
const componentName = path.basename(dir);
|
|
62
|
+
|
|
63
|
+
// Find the main component file
|
|
64
|
+
const mainFile = contents.find(f =>
|
|
65
|
+
!f.isDirectory &&
|
|
66
|
+
this.isComponentFile(f.name, componentName)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!mainFile) {
|
|
70
|
+
continue; // Skip if no main component file found
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for required files
|
|
74
|
+
for (const filePattern of colocationConfig.requiredFiles) {
|
|
75
|
+
if (!filePattern.required) continue;
|
|
76
|
+
|
|
77
|
+
const expectedPattern = this.expandPattern(filePattern.pattern, componentName);
|
|
78
|
+
const matchingFile = contents.find(f =>
|
|
79
|
+
!f.isDirectory && micromatch.isMatch(f.name, expectedPattern)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!matchingFile) {
|
|
83
|
+
const error: ValidationError = {
|
|
84
|
+
type: 'missing-file',
|
|
85
|
+
severity: this.config.severity || 'error',
|
|
86
|
+
message: `Missing required file: ${filePattern.description || expectedPattern}`,
|
|
87
|
+
directory: dir,
|
|
88
|
+
expected: expectedPattern,
|
|
89
|
+
suggestion: `Create ${expectedPattern} in ${dir}/`
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.addError(error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate naming convention for component
|
|
97
|
+
if (colocationConfig.namingConvention) {
|
|
98
|
+
const isValid = validateNamingConvention(
|
|
99
|
+
componentName,
|
|
100
|
+
colocationConfig.namingConvention
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!isValid) {
|
|
104
|
+
const expected = getExpectedNaming(
|
|
105
|
+
componentName,
|
|
106
|
+
colocationConfig.namingConvention
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const error: ValidationError = {
|
|
110
|
+
type: 'naming-violation',
|
|
111
|
+
severity: this.config.severity || 'error',
|
|
112
|
+
message: `Component directory name doesn't follow ${colocationConfig.namingConvention} convention`,
|
|
113
|
+
directory: dir,
|
|
114
|
+
actual: componentName,
|
|
115
|
+
expected,
|
|
116
|
+
suggestion: `Rename directory to ${expected}`
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.addError(error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async validateFileNaming(files: ScannedFile[]): Promise<void> {
|
|
126
|
+
const namingRules = this.config.rules?.fileNaming;
|
|
127
|
+
if (!namingRules) return;
|
|
128
|
+
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
if (file.isDirectory) continue;
|
|
131
|
+
|
|
132
|
+
// Check each naming rule pattern
|
|
133
|
+
for (const [pattern, rule] of Object.entries(namingRules)) {
|
|
134
|
+
if (micromatch.isMatch(file.path, pattern)) {
|
|
135
|
+
const isValid = validateNamingConvention(file.name, rule.convention);
|
|
136
|
+
|
|
137
|
+
if (!isValid) {
|
|
138
|
+
const expected = getExpectedNaming(file.name, rule.convention);
|
|
139
|
+
|
|
140
|
+
const error: ValidationError = {
|
|
141
|
+
type: 'naming-violation',
|
|
142
|
+
severity: rule.severity || this.config.severity || 'error',
|
|
143
|
+
message: `File name doesn't follow ${rule.convention} convention`,
|
|
144
|
+
file: file.path,
|
|
145
|
+
actual: file.name,
|
|
146
|
+
expected,
|
|
147
|
+
suggestion: `Rename to ${expected}`
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
this.addError(error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async validateFolderStructure(files: ScannedFile[]): Promise<void> {
|
|
158
|
+
const folderRules = this.config.rules?.folderStructure;
|
|
159
|
+
if (!folderRules) return;
|
|
160
|
+
|
|
161
|
+
for (const rule of folderRules) {
|
|
162
|
+
// Check if required folder exists
|
|
163
|
+
if (!this.scanner.isDirectory(rule.path)) {
|
|
164
|
+
const error: ValidationError = {
|
|
165
|
+
type: 'structure-violation',
|
|
166
|
+
severity: this.config.severity || 'error',
|
|
167
|
+
message: `Required directory not found: ${rule.name}`,
|
|
168
|
+
directory: rule.path,
|
|
169
|
+
suggestion: `Create directory at ${rule.path}`
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
this.addError(error);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get files in this directory
|
|
177
|
+
const dirFiles = files.filter(f =>
|
|
178
|
+
f.path.startsWith(rule.path) && !f.isDirectory
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Validate file extensions
|
|
182
|
+
if (rule.allowedExtensions) {
|
|
183
|
+
for (const file of dirFiles) {
|
|
184
|
+
if (!rule.allowedExtensions.includes(file.extension)) {
|
|
185
|
+
const error: ValidationError = {
|
|
186
|
+
type: 'forbidden-file',
|
|
187
|
+
severity: 'warning',
|
|
188
|
+
message: `File extension ${file.extension} not allowed in ${rule.name}`,
|
|
189
|
+
file: file.path,
|
|
190
|
+
suggestion: `Allowed extensions: ${rule.allowedExtensions.join(', ')}`
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
this.addError(error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate naming convention for files in this directory
|
|
199
|
+
if (rule.namingConvention) {
|
|
200
|
+
for (const file of dirFiles) {
|
|
201
|
+
const isValid = validateNamingConvention(file.name, rule.namingConvention);
|
|
202
|
+
|
|
203
|
+
if (!isValid) {
|
|
204
|
+
const expected = getExpectedNaming(file.name, rule.namingConvention);
|
|
205
|
+
|
|
206
|
+
const error: ValidationError = {
|
|
207
|
+
type: 'naming-violation',
|
|
208
|
+
severity: this.config.severity || 'error',
|
|
209
|
+
message: `File in ${rule.name} doesn't follow ${rule.namingConvention} convention`,
|
|
210
|
+
file: file.path,
|
|
211
|
+
actual: file.name,
|
|
212
|
+
expected,
|
|
213
|
+
suggestion: `Rename to ${expected}`
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
this.addError(error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private isComponentFile(filename: string, componentName: string): boolean {
|
|
224
|
+
const nameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
|
225
|
+
return nameWithoutExt === componentName ||
|
|
226
|
+
nameWithoutExt.toLowerCase() === componentName.toLowerCase();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private expandPattern(pattern: string, componentName: string): string {
|
|
230
|
+
return pattern.replace(/\*/g, componentName);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private addError(error: ValidationError): void {
|
|
234
|
+
if (error.severity === 'warning') {
|
|
235
|
+
this.warnings.push(error);
|
|
236
|
+
} else {
|
|
237
|
+
this.errors.push(error);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Made with
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Preset } from '../types';
|
|
2
|
+
import { reactPreset } from './react';
|
|
3
|
+
|
|
4
|
+
export const presets: Record<string, Preset> = {
|
|
5
|
+
react: reactPreset
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function getPreset(name: string): Preset | undefined {
|
|
9
|
+
return presets[name];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function listPresets(): string[] {
|
|
13
|
+
return Object.keys(presets);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Made with
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Preset } from '../types';
|
|
2
|
+
|
|
3
|
+
export const reactPreset: Preset = {
|
|
4
|
+
name: 'react',
|
|
5
|
+
description: 'React project with component co-location and testing requirements',
|
|
6
|
+
config: {
|
|
7
|
+
preset: 'react',
|
|
8
|
+
rootDir: 'src',
|
|
9
|
+
rules: {
|
|
10
|
+
componentColocation: {
|
|
11
|
+
enabled: true,
|
|
12
|
+
componentDirs: ['components', 'pages', 'features'],
|
|
13
|
+
requiredFiles: [
|
|
14
|
+
{
|
|
15
|
+
pattern: '*.test.{ts,tsx,js,jsx}',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'Test file for component'
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pattern: '*.stories.{ts,tsx,js,jsx}',
|
|
21
|
+
required: true,
|
|
22
|
+
description: 'Storybook story file'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: 'index.{ts,tsx,js,jsx}',
|
|
26
|
+
required: false,
|
|
27
|
+
description: 'Index file for re-exports'
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
namingConvention: 'PascalCase'
|
|
31
|
+
},
|
|
32
|
+
fileNaming: {
|
|
33
|
+
'components/**/*.{tsx,jsx}': {
|
|
34
|
+
convention: 'PascalCase',
|
|
35
|
+
severity: 'error'
|
|
36
|
+
},
|
|
37
|
+
'hooks/**/*.{ts,tsx,js,jsx}': {
|
|
38
|
+
convention: 'camelCase',
|
|
39
|
+
severity: 'error'
|
|
40
|
+
},
|
|
41
|
+
'utils/**/*.{ts,js}': {
|
|
42
|
+
convention: 'camelCase',
|
|
43
|
+
severity: 'error'
|
|
44
|
+
},
|
|
45
|
+
'types/**/*.{ts,tsx}': {
|
|
46
|
+
convention: 'PascalCase',
|
|
47
|
+
severity: 'error'
|
|
48
|
+
},
|
|
49
|
+
'constants/**/*.{ts,js}': {
|
|
50
|
+
convention: 'UPPER_CASE',
|
|
51
|
+
severity: 'warning'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
folderStructure: [
|
|
55
|
+
{
|
|
56
|
+
name: 'components',
|
|
57
|
+
path: 'src/components',
|
|
58
|
+
namingConvention: 'PascalCase',
|
|
59
|
+
allowedExtensions: ['.tsx', '.ts', '.jsx', '.js', '.css', '.scss', '.module.css']
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'hooks',
|
|
63
|
+
path: 'src/hooks',
|
|
64
|
+
namingConvention: 'camelCase',
|
|
65
|
+
allowedExtensions: ['.ts', '.tsx', '.js', '.jsx']
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'utils',
|
|
69
|
+
path: 'src/utils',
|
|
70
|
+
namingConvention: 'camelCase',
|
|
71
|
+
allowedExtensions: ['.ts', '.js']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'types',
|
|
75
|
+
path: 'src/types',
|
|
76
|
+
namingConvention: 'PascalCase',
|
|
77
|
+
allowedExtensions: ['.ts', '.d.ts']
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
ignore: [
|
|
82
|
+
'**/node_modules/**',
|
|
83
|
+
'**/dist/**',
|
|
84
|
+
'**/build/**',
|
|
85
|
+
'**/.next/**',
|
|
86
|
+
'**/coverage/**',
|
|
87
|
+
'**/.git/**'
|
|
88
|
+
],
|
|
89
|
+
severity: 'error'
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Made with
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ValidationResult, ValidationError } from '../types';
|
|
3
|
+
|
|
4
|
+
export class ConsoleReporter {
|
|
5
|
+
report(result: ValidationResult): void {
|
|
6
|
+
console.log('\n' + chalk.bold('Project Structure Validation Results'));
|
|
7
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
8
|
+
|
|
9
|
+
console.log(chalk.gray(`Files scanned: ${result.filesScanned}`));
|
|
10
|
+
console.log(chalk.gray(`Directories scanned: ${result.directoriesScanned}`));
|
|
11
|
+
console.log('');
|
|
12
|
+
|
|
13
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
14
|
+
console.log(chalk.green('✓ No issues found!'));
|
|
15
|
+
console.log('');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Report errors
|
|
20
|
+
if (result.errors.length > 0) {
|
|
21
|
+
console.log(chalk.red.bold(`✗ ${result.errors.length} Error${result.errors.length > 1 ? 's' : ''}`));
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
result.errors.forEach((error, index) => {
|
|
25
|
+
this.printError(error, index + 1);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Report warnings
|
|
30
|
+
if (result.warnings.length > 0) {
|
|
31
|
+
console.log(chalk.yellow.bold(`⚠ ${result.warnings.length} Warning${result.warnings.length > 1 ? 's' : ''}`));
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
result.warnings.forEach((warning, index) => {
|
|
35
|
+
this.printWarning(warning, index + 1);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
40
|
+
|
|
41
|
+
if (result.errors.length > 0) {
|
|
42
|
+
console.log(chalk.red.bold(`\n✗ Validation failed with ${result.errors.length} error${result.errors.length > 1 ? 's' : ''}`));
|
|
43
|
+
} else {
|
|
44
|
+
console.log(chalk.yellow.bold(`\n⚠ Validation passed with ${result.warnings.length} warning${result.warnings.length > 1 ? 's' : ''}`));
|
|
45
|
+
}
|
|
46
|
+
console.log('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private printError(error: ValidationError, index: number): void {
|
|
50
|
+
console.log(chalk.red(`${index}. ${error.message}`));
|
|
51
|
+
|
|
52
|
+
if (error.file) {
|
|
53
|
+
console.log(chalk.gray(` File: ${error.file}`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (error.directory) {
|
|
57
|
+
console.log(chalk.gray(` Directory: ${error.directory}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (error.actual) {
|
|
61
|
+
console.log(chalk.gray(` Actual: ${error.actual}`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error.expected) {
|
|
65
|
+
console.log(chalk.gray(` Expected: ${error.expected}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error.suggestion) {
|
|
69
|
+
console.log(chalk.cyan(` 💡 ${error.suggestion}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private printWarning(warning: ValidationError, index: number): void {
|
|
76
|
+
console.log(chalk.yellow(`${index}. ${warning.message}`));
|
|
77
|
+
|
|
78
|
+
if (warning.file) {
|
|
79
|
+
console.log(chalk.gray(` File: ${warning.file}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (warning.directory) {
|
|
83
|
+
console.log(chalk.gray(` Directory: ${warning.directory}`));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (warning.actual) {
|
|
87
|
+
console.log(chalk.gray(` Actual: ${warning.actual}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (warning.expected) {
|
|
91
|
+
console.log(chalk.gray(` Expected: ${warning.expected}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (warning.suggestion) {
|
|
95
|
+
console.log(chalk.cyan(` 💡 ${warning.suggestion}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
reportConfigError(error: string): void {
|
|
102
|
+
console.log(chalk.red.bold('\n✗ Configuration Error'));
|
|
103
|
+
console.log(chalk.red(error));
|
|
104
|
+
console.log('');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
reportConfigErrors(errors: string[]): void {
|
|
108
|
+
console.log(chalk.red.bold('\n✗ Configuration Errors'));
|
|
109
|
+
errors.forEach(error => {
|
|
110
|
+
console.log(chalk.red(` • ${error}`));
|
|
111
|
+
});
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Made with
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type NamingConvention = 'PascalCase' | 'camelCase' | 'kebab-case' | 'snake_case' | 'UPPER_CASE';
|
|
2
|
+
|
|
3
|
+
export type Severity = 'error' | 'warning';
|
|
4
|
+
|
|
5
|
+
export interface FilePattern {
|
|
6
|
+
pattern: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FolderRule {
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
requiredFiles?: FilePattern[];
|
|
15
|
+
namingConvention?: NamingConvention;
|
|
16
|
+
allowedExtensions?: string[];
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProjectConfig {
|
|
21
|
+
preset?: string;
|
|
22
|
+
rootDir?: string;
|
|
23
|
+
rules?: {
|
|
24
|
+
folderStructure?: FolderRule[];
|
|
25
|
+
componentColocation?: {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
componentDirs: string[];
|
|
28
|
+
requiredFiles: FilePattern[];
|
|
29
|
+
namingConvention?: NamingConvention;
|
|
30
|
+
};
|
|
31
|
+
fileNaming?: {
|
|
32
|
+
[pattern: string]: {
|
|
33
|
+
convention: NamingConvention;
|
|
34
|
+
severity?: Severity;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
ignore?: string[];
|
|
39
|
+
severity?: Severity;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ValidationError {
|
|
43
|
+
type: 'missing-file' | 'naming-violation' | 'structure-violation' | 'forbidden-file';
|
|
44
|
+
severity: Severity;
|
|
45
|
+
message: string;
|
|
46
|
+
file?: string;
|
|
47
|
+
directory?: string;
|
|
48
|
+
expected?: string;
|
|
49
|
+
actual?: string;
|
|
50
|
+
suggestion?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ValidationResult {
|
|
54
|
+
valid: boolean;
|
|
55
|
+
errors: ValidationError[];
|
|
56
|
+
warnings: ValidationError[];
|
|
57
|
+
filesScanned: number;
|
|
58
|
+
directoriesScanned: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Preset {
|
|
62
|
+
name: string;
|
|
63
|
+
description: string;
|
|
64
|
+
config: ProjectConfig;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Made with
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
declare module 'micromatch' {
|
|
2
|
+
function micromatch(
|
|
3
|
+
list: string | string[],
|
|
4
|
+
patterns: string | string[],
|
|
5
|
+
options?: micromatch.Options
|
|
6
|
+
): string[];
|
|
7
|
+
|
|
8
|
+
namespace micromatch {
|
|
9
|
+
interface Options {
|
|
10
|
+
ignore?: string | string[];
|
|
11
|
+
matchBase?: boolean;
|
|
12
|
+
nobrace?: boolean;
|
|
13
|
+
nocase?: boolean;
|
|
14
|
+
noext?: boolean;
|
|
15
|
+
noglobstar?: boolean;
|
|
16
|
+
nonull?: boolean;
|
|
17
|
+
dot?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isMatch(
|
|
21
|
+
str: string,
|
|
22
|
+
patterns: string | string[],
|
|
23
|
+
options?: Options
|
|
24
|
+
): boolean;
|
|
25
|
+
|
|
26
|
+
function makeRe(
|
|
27
|
+
pattern: string,
|
|
28
|
+
options?: Options
|
|
29
|
+
): RegExp;
|
|
30
|
+
|
|
31
|
+
function match(
|
|
32
|
+
list: string[],
|
|
33
|
+
patterns: string | string[],
|
|
34
|
+
options?: Options
|
|
35
|
+
): string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export = micromatch;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Made with
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { validateNamingConvention, getExpectedNaming } from '../naming';
|
|
2
|
+
|
|
3
|
+
describe('validateNamingConvention', () => {
|
|
4
|
+
describe('PascalCase', () => {
|
|
5
|
+
it('should validate correct PascalCase names', () => {
|
|
6
|
+
expect(validateNamingConvention('Button.tsx', 'PascalCase')).toBe(true);
|
|
7
|
+
expect(validateNamingConvention('MyComponent.tsx', 'PascalCase')).toBe(true);
|
|
8
|
+
expect(validateNamingConvention('UserProfile.tsx', 'PascalCase')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should reject incorrect PascalCase names', () => {
|
|
12
|
+
expect(validateNamingConvention('button.tsx', 'PascalCase')).toBe(false);
|
|
13
|
+
expect(validateNamingConvention('myComponent.tsx', 'PascalCase')).toBe(false);
|
|
14
|
+
expect(validateNamingConvention('user-profile.tsx', 'PascalCase')).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('camelCase', () => {
|
|
19
|
+
it('should validate correct camelCase names', () => {
|
|
20
|
+
expect(validateNamingConvention('useAuth.ts', 'camelCase')).toBe(true);
|
|
21
|
+
expect(validateNamingConvention('formatDate.ts', 'camelCase')).toBe(true);
|
|
22
|
+
expect(validateNamingConvention('getUserData.ts', 'camelCase')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should reject incorrect camelCase names', () => {
|
|
26
|
+
expect(validateNamingConvention('UseAuth.ts', 'camelCase')).toBe(false);
|
|
27
|
+
expect(validateNamingConvention('format-date.ts', 'camelCase')).toBe(false);
|
|
28
|
+
expect(validateNamingConvention('get_user_data.ts', 'camelCase')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('kebab-case', () => {
|
|
33
|
+
it('should validate correct kebab-case names', () => {
|
|
34
|
+
expect(validateNamingConvention('my-component.tsx', 'kebab-case')).toBe(true);
|
|
35
|
+
expect(validateNamingConvention('user-profile.tsx', 'kebab-case')).toBe(true);
|
|
36
|
+
expect(validateNamingConvention('format-date.ts', 'kebab-case')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should reject incorrect kebab-case names', () => {
|
|
40
|
+
expect(validateNamingConvention('MyComponent.tsx', 'kebab-case')).toBe(false);
|
|
41
|
+
expect(validateNamingConvention('userProfile.tsx', 'kebab-case')).toBe(false);
|
|
42
|
+
expect(validateNamingConvention('format_date.ts', 'kebab-case')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('snake_case', () => {
|
|
47
|
+
it('should validate correct snake_case names', () => {
|
|
48
|
+
expect(validateNamingConvention('my_component.tsx', 'snake_case')).toBe(true);
|
|
49
|
+
expect(validateNamingConvention('user_profile.tsx', 'snake_case')).toBe(true);
|
|
50
|
+
expect(validateNamingConvention('format_date.ts', 'snake_case')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should reject incorrect snake_case names', () => {
|
|
54
|
+
expect(validateNamingConvention('MyComponent.tsx', 'snake_case')).toBe(false);
|
|
55
|
+
expect(validateNamingConvention('userProfile.tsx', 'snake_case')).toBe(false);
|
|
56
|
+
expect(validateNamingConvention('format-date.ts', 'snake_case')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('UPPER_CASE', () => {
|
|
61
|
+
it('should validate correct UPPER_CASE names', () => {
|
|
62
|
+
expect(validateNamingConvention('API_CONSTANTS.ts', 'UPPER_CASE')).toBe(true);
|
|
63
|
+
expect(validateNamingConvention('USER_ROLES.ts', 'UPPER_CASE')).toBe(true);
|
|
64
|
+
expect(validateNamingConvention('MAX_LENGTH.ts', 'UPPER_CASE')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject incorrect UPPER_CASE names', () => {
|
|
68
|
+
expect(validateNamingConvention('apiConstants.ts', 'UPPER_CASE')).toBe(false);
|
|
69
|
+
expect(validateNamingConvention('UserRoles.ts', 'UPPER_CASE')).toBe(false);
|
|
70
|
+
expect(validateNamingConvention('max-length.ts', 'UPPER_CASE')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getExpectedNaming', () => {
|
|
76
|
+
it('should convert to PascalCase', () => {
|
|
77
|
+
expect(getExpectedNaming('my-component.tsx', 'PascalCase')).toBe('MyComponent.tsx');
|
|
78
|
+
expect(getExpectedNaming('user_profile.tsx', 'PascalCase')).toBe('UserProfile.tsx');
|
|
79
|
+
expect(getExpectedNaming('formatDate.ts', 'PascalCase')).toBe('FormatDate.ts');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should convert to camelCase', () => {
|
|
83
|
+
expect(getExpectedNaming('MyComponent.tsx', 'camelCase')).toBe('myComponent.tsx');
|
|
84
|
+
expect(getExpectedNaming('user-profile.tsx', 'camelCase')).toBe('userProfile.tsx');
|
|
85
|
+
expect(getExpectedNaming('format_date.ts', 'camelCase')).toBe('formatDate.ts');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should convert to kebab-case', () => {
|
|
89
|
+
expect(getExpectedNaming('MyComponent.tsx', 'kebab-case')).toBe('my-component.tsx');
|
|
90
|
+
expect(getExpectedNaming('userProfile.tsx', 'kebab-case')).toBe('user-profile.tsx');
|
|
91
|
+
expect(getExpectedNaming('formatDate.ts', 'kebab-case')).toBe('format-date.ts');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should convert to snake_case', () => {
|
|
95
|
+
expect(getExpectedNaming('MyComponent.tsx', 'snake_case')).toBe('my_component.tsx');
|
|
96
|
+
expect(getExpectedNaming('userProfile.tsx', 'snake_case')).toBe('user_profile.tsx');
|
|
97
|
+
expect(getExpectedNaming('formatDate.ts', 'snake_case')).toBe('format_date.ts');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should convert to UPPER_CASE', () => {
|
|
101
|
+
expect(getExpectedNaming('myComponent.tsx', 'UPPER_CASE')).toBe('MY_COMPONENT.tsx');
|
|
102
|
+
expect(getExpectedNaming('userProfile.tsx', 'UPPER_CASE')).toBe('USER_PROFILE.tsx');
|
|
103
|
+
expect(getExpectedNaming('formatDate.ts', 'UPPER_CASE')).toBe('FORMAT_DATE.ts');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Made with
|