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,162 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import micromatch from 'micromatch';
|
|
5
|
+
|
|
6
|
+
export interface ScannedFile {
|
|
7
|
+
path: string;
|
|
8
|
+
name: string;
|
|
9
|
+
directory: string;
|
|
10
|
+
extension: string;
|
|
11
|
+
isDirectory: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class FileScanner {
|
|
15
|
+
private rootDir: string;
|
|
16
|
+
private ignorePatterns: string[];
|
|
17
|
+
|
|
18
|
+
constructor(rootDir: string, ignorePatterns: string[] = []) {
|
|
19
|
+
this.rootDir = path.resolve(rootDir);
|
|
20
|
+
this.ignorePatterns = ignorePatterns;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async scanDirectory(dir: string = this.rootDir): Promise<ScannedFile[]> {
|
|
24
|
+
const files: ScannedFile[] = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(dir, entry.name);
|
|
31
|
+
const relativePath = path.relative(this.rootDir, fullPath);
|
|
32
|
+
|
|
33
|
+
// Skip ignored paths
|
|
34
|
+
if (this.shouldIgnore(relativePath)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const scannedFile: ScannedFile = {
|
|
39
|
+
path: relativePath,
|
|
40
|
+
name: entry.name,
|
|
41
|
+
directory: path.relative(this.rootDir, dir),
|
|
42
|
+
extension: path.extname(entry.name),
|
|
43
|
+
isDirectory: entry.isDirectory()
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
files.push(scannedFile);
|
|
47
|
+
|
|
48
|
+
// Recursively scan subdirectories
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
const subFiles = await this.scanDirectory(fullPath);
|
|
51
|
+
files.push(...subFiles);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`Error scanning directory ${dir}:`, error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async findFiles(pattern: string, baseDir?: string): Promise<string[]> {
|
|
62
|
+
const searchDir = baseDir ? path.join(this.rootDir, baseDir) : this.rootDir;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const files = await glob(pattern, {
|
|
66
|
+
cwd: searchDir,
|
|
67
|
+
ignore: this.ignorePatterns,
|
|
68
|
+
nodir: true
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return files.map(file =>
|
|
72
|
+
baseDir ? path.join(baseDir, file) : file
|
|
73
|
+
);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`Error finding files with pattern ${pattern}:`, error);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getDirectoryContents(dir: string): Promise<ScannedFile[]> {
|
|
81
|
+
const fullPath = path.join(this.rootDir, dir);
|
|
82
|
+
const files: ScannedFile[] = [];
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const entries = await fs.promises.readdir(fullPath, { withFileTypes: true });
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const relativePath = path.join(dir, entry.name);
|
|
89
|
+
|
|
90
|
+
if (this.shouldIgnore(relativePath)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
files.push({
|
|
95
|
+
path: relativePath,
|
|
96
|
+
name: entry.name,
|
|
97
|
+
directory: dir,
|
|
98
|
+
extension: path.extname(entry.name),
|
|
99
|
+
isDirectory: entry.isDirectory()
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Directory doesn't exist or can't be read
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getComponentDirectories(componentDirs: string[]): string[] {
|
|
111
|
+
const dirs: string[] = [];
|
|
112
|
+
|
|
113
|
+
for (const dir of componentDirs) {
|
|
114
|
+
const fullPath = path.join(this.rootDir, dir);
|
|
115
|
+
|
|
116
|
+
if (!fs.existsSync(fullPath)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
122
|
+
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
const relativePath = path.join(dir, entry.name);
|
|
126
|
+
if (!this.shouldIgnore(relativePath)) {
|
|
127
|
+
dirs.push(relativePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`Error reading component directory ${fullPath}:`, error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return dirs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private shouldIgnore(filePath: string): boolean {
|
|
140
|
+
if (this.ignorePatterns.length === 0) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return micromatch.isMatch(filePath, this.ignorePatterns);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fileExists(filePath: string): boolean {
|
|
148
|
+
const fullPath = path.join(this.rootDir, filePath);
|
|
149
|
+
return fs.existsSync(fullPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
isDirectory(filePath: string): boolean {
|
|
153
|
+
const fullPath = path.join(this.rootDir, filePath);
|
|
154
|
+
try {
|
|
155
|
+
return fs.statSync(fullPath).isDirectory();
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Made with
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NamingConvention } from '../types';
|
|
2
|
+
|
|
3
|
+
export function validateNamingConvention(
|
|
4
|
+
filename: string,
|
|
5
|
+
convention: NamingConvention
|
|
6
|
+
): boolean {
|
|
7
|
+
// Remove extension
|
|
8
|
+
const nameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
|
9
|
+
|
|
10
|
+
// Remove test/spec/stories suffixes but keep the base name
|
|
11
|
+
const cleanName = nameWithoutExt
|
|
12
|
+
.replace(/\.(test|spec|stories|types|d)$/, '');
|
|
13
|
+
|
|
14
|
+
switch (convention) {
|
|
15
|
+
case 'PascalCase':
|
|
16
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(cleanName);
|
|
17
|
+
|
|
18
|
+
case 'camelCase':
|
|
19
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(cleanName);
|
|
20
|
+
|
|
21
|
+
case 'kebab-case':
|
|
22
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(cleanName);
|
|
23
|
+
|
|
24
|
+
case 'snake_case':
|
|
25
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(cleanName);
|
|
26
|
+
|
|
27
|
+
case 'UPPER_CASE':
|
|
28
|
+
return /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/.test(cleanName);
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getExpectedNaming(
|
|
36
|
+
filename: string,
|
|
37
|
+
convention: NamingConvention
|
|
38
|
+
): string {
|
|
39
|
+
const nameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
|
40
|
+
const ext = filename.substring(nameWithoutExt.length);
|
|
41
|
+
|
|
42
|
+
let converted = nameWithoutExt;
|
|
43
|
+
|
|
44
|
+
switch (convention) {
|
|
45
|
+
case 'PascalCase':
|
|
46
|
+
converted = toPascalCase(nameWithoutExt);
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case 'camelCase':
|
|
50
|
+
converted = toCamelCase(nameWithoutExt);
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case 'kebab-case':
|
|
54
|
+
converted = toKebabCase(nameWithoutExt);
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case 'snake_case':
|
|
58
|
+
converted = toSnakeCase(nameWithoutExt);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case 'UPPER_CASE':
|
|
62
|
+
converted = toUpperCase(nameWithoutExt);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return converted + ext;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toPascalCase(str: string): string {
|
|
70
|
+
return str
|
|
71
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
72
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toCamelCase(str: string): string {
|
|
76
|
+
return str
|
|
77
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
78
|
+
.replace(/^(.)/, (c) => c.toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toKebabCase(str: string): string {
|
|
82
|
+
return str
|
|
83
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
84
|
+
.replace(/[\s_]+/g, '-')
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toSnakeCase(str: string): string {
|
|
89
|
+
return str
|
|
90
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
91
|
+
.replace(/[\s-]+/g, '_')
|
|
92
|
+
.toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toUpperCase(str: string): string {
|
|
96
|
+
return toSnakeCase(str).toUpperCase();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Made with
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020", "DOM"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"moduleResolution": "node"
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
+
}
|