@empline/preflight 1.1.14 → 1.1.15
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/dist/checks/auth/session-provider-wrapper.d.ts +47 -0
- package/dist/checks/auth/session-provider-wrapper.d.ts.map +1 -0
- package/dist/checks/auth/session-provider-wrapper.js +286 -0
- package/dist/checks/auth/session-provider-wrapper.js.map +1 -0
- package/dist/checks/database/prisma-upsert-safety.d.ts +39 -0
- package/dist/checks/database/prisma-upsert-safety.d.ts.map +1 -0
- package/dist/checks/database/prisma-upsert-safety.js +220 -0
- package/dist/checks/database/prisma-upsert-safety.js.map +1 -0
- package/dist/checks/dependencies/dependency-health-monitor.d.ts +49 -0
- package/dist/checks/dependencies/dependency-health-monitor.d.ts.map +1 -0
- package/dist/checks/dependencies/dependency-health-monitor.js +323 -0
- package/dist/checks/dependencies/dependency-health-monitor.js.map +1 -0
- package/dist/checks/file-hygiene-validation.d.ts +31 -0
- package/dist/checks/file-hygiene-validation.d.ts.map +1 -0
- package/dist/checks/file-hygiene-validation.js +934 -0
- package/dist/checks/file-hygiene-validation.js.map +1 -0
- package/dist/checks/organization/file-cleanup-validation.d.ts +22 -0
- package/dist/checks/organization/file-cleanup-validation.d.ts.map +1 -0
- package/dist/checks/organization/file-cleanup-validation.js +1121 -0
- package/dist/checks/organization/file-cleanup-validation.js.map +1 -0
- package/dist/checks/runtime/tailwind-runtime-check.d.ts +36 -0
- package/dist/checks/runtime/tailwind-runtime-check.d.ts.map +1 -0
- package/dist/checks/runtime/tailwind-runtime-check.js +264 -0
- package/dist/checks/runtime/tailwind-runtime-check.js.map +1 -0
- package/dist/checks/shipping-integration-validation.d.ts +28 -0
- package/dist/checks/shipping-integration-validation.d.ts.map +1 -0
- package/dist/checks/shipping-integration-validation.js +409 -0
- package/dist/checks/shipping-integration-validation.js.map +1 -0
- package/dist/checks/system/layout-constants-sync.d.ts +36 -0
- package/dist/checks/system/layout-constants-sync.d.ts.map +1 -0
- package/dist/checks/system/layout-constants-sync.js +642 -0
- package/dist/checks/system/layout-constants-sync.js.map +1 -0
- package/dist/checks/system/preflight-circular-dependency-detector.d.ts +26 -0
- package/dist/checks/system/preflight-circular-dependency-detector.d.ts.map +1 -0
- package/dist/checks/system/preflight-circular-dependency-detector.js +310 -0
- package/dist/checks/system/preflight-circular-dependency-detector.js.map +1 -0
- package/dist/checks/system/preflight-execution-benchmarks.d.ts +24 -0
- package/dist/checks/system/preflight-execution-benchmarks.d.ts.map +1 -0
- package/dist/checks/system/preflight-execution-benchmarks.js +282 -0
- package/dist/checks/system/preflight-execution-benchmarks.js.map +1 -0
- package/dist/checks/system/preflight-tag-taxonomy-validator.d.ts +27 -0
- package/dist/checks/system/preflight-tag-taxonomy-validator.d.ts.map +1 -0
- package/dist/checks/system/preflight-tag-taxonomy-validator.js +361 -0
- package/dist/checks/system/preflight-tag-taxonomy-validator.js.map +1 -0
- package/dist/utils/console-chars.d.ts +16 -0
- package/dist/utils/console-chars.d.ts.map +1 -1
- package/dist/utils/console-chars.js +10 -0
- package/dist/utils/console-chars.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* File Hygiene Validation Preflight (BLOCKING)
|
|
5
|
+
*
|
|
6
|
+
* Catches file naming anti-patterns that indicate technical debt:
|
|
7
|
+
* 1. Version suffixes (-v2, -v3, etc.) - indicates unfinished migration
|
|
8
|
+
* 2. Temporary suffixes (-old, -backup, -new, -legacy, -deprecated)
|
|
9
|
+
* 3. Problematic route segments (/new/, /old/, /temp/) in app router
|
|
10
|
+
* 4. Duplicate/shadow files that should be consolidated
|
|
11
|
+
* 5. Case collisions (UserCard.tsx vs usercard.tsx)
|
|
12
|
+
* 6. Component/export name mismatches
|
|
13
|
+
* 7. Directory naming violations
|
|
14
|
+
*
|
|
15
|
+
* BLOCKING: Fails build if violations found (unless allowlisted).
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* pnpm preflight:file-hygiene - BLOCKING mode
|
|
19
|
+
* pnpm preflight:file-hygiene --warning - Warning mode
|
|
20
|
+
* pnpm preflight:file-hygiene --fix-preview - Show migration suggestions
|
|
21
|
+
* pnpm preflight:file-hygiene --add-allowlist <file> - Add to allowlist
|
|
22
|
+
* pnpm preflight:file-hygiene --check-duplicates - Check for duplicate filenames
|
|
23
|
+
* pnpm preflight:file-hygiene --check-exports - Check component/export name match
|
|
24
|
+
*/
|
|
25
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
28
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
29
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
30
|
+
}
|
|
31
|
+
Object.defineProperty(o, k2, desc);
|
|
32
|
+
}) : (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
o[k2] = m[k];
|
|
35
|
+
}));
|
|
36
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
37
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
38
|
+
}) : function(o, v) {
|
|
39
|
+
o["default"] = v;
|
|
40
|
+
});
|
|
41
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
42
|
+
var ownKeys = function(o) {
|
|
43
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
44
|
+
var ar = [];
|
|
45
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
46
|
+
return ar;
|
|
47
|
+
};
|
|
48
|
+
return ownKeys(o);
|
|
49
|
+
};
|
|
50
|
+
return function (mod) {
|
|
51
|
+
if (mod && mod.__esModule) return mod;
|
|
52
|
+
var result = {};
|
|
53
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
54
|
+
__setModuleDefault(result, mod);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
exports.tags = exports.description = exports.blocking = exports.category = exports.name = exports.id = void 0;
|
|
60
|
+
exports.run = run;
|
|
61
|
+
const fs = __importStar(require("fs"));
|
|
62
|
+
const path = __importStar(require("path"));
|
|
63
|
+
const console_chars_1 = require("../utils/console-chars");
|
|
64
|
+
const findings_writer_1 = require("../utils/findings-writer");
|
|
65
|
+
const universal_progress_reporter_1 = require("./system/universal-progress-reporter");
|
|
66
|
+
const glob_1 = require("glob");
|
|
67
|
+
// Check metadata
|
|
68
|
+
exports.id = "misc/file-hygiene-validation";
|
|
69
|
+
exports.name = "File Hygiene Validation";
|
|
70
|
+
exports.category = "misc";
|
|
71
|
+
exports.blocking = true;
|
|
72
|
+
exports.description = "File Hygiene Validation Preflight (BLOCKING)";
|
|
73
|
+
exports.tags = ["misc", "validation"];
|
|
74
|
+
// CONFIGURATION
|
|
75
|
+
const CONFIG = {
|
|
76
|
+
// Directories to scan
|
|
77
|
+
includeDirs: ["app", "components", "lib", "hooks", "contexts", "types", "scripts", "packages"],
|
|
78
|
+
// Files/directories to skip
|
|
79
|
+
excludePatterns: [
|
|
80
|
+
"**/node_modules/**",
|
|
81
|
+
"**/.next/**",
|
|
82
|
+
"**/dist/**",
|
|
83
|
+
"**/coverage/**",
|
|
84
|
+
"**/.git/**",
|
|
85
|
+
"**/test-results/**",
|
|
86
|
+
"**/backups/**",
|
|
87
|
+
"**/prisma/migrations/**",
|
|
88
|
+
],
|
|
89
|
+
// Anti-patterns in filenames (version suffixes, temp markers)
|
|
90
|
+
filenameAntiPatterns: [
|
|
91
|
+
{
|
|
92
|
+
pattern: /-v\d+\.(ts|tsx|js|jsx|mjs)$/i,
|
|
93
|
+
name: "version-suffix",
|
|
94
|
+
description: "Version suffix in filename (-v2, -v3)",
|
|
95
|
+
severity: "error",
|
|
96
|
+
suggestion: "Rename to canonical name and update all imports",
|
|
97
|
+
excludePathPatterns: [],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
pattern: /-old\.(ts|tsx|js|jsx|mjs)$/i,
|
|
101
|
+
name: "old-suffix",
|
|
102
|
+
description: "Old suffix indicates deprecated file",
|
|
103
|
+
severity: "error",
|
|
104
|
+
suggestion: "Delete if unused, or rename and consolidate",
|
|
105
|
+
excludePathPatterns: [],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
// Match files ending with -backup.ts but NOT files that are backup tools
|
|
109
|
+
pattern: /[^e]-backup\.(ts|tsx|js|jsx|mjs)$/i,
|
|
110
|
+
name: "backup-suffix",
|
|
111
|
+
description: "Backup suffix indicates temporary file",
|
|
112
|
+
severity: "error",
|
|
113
|
+
suggestion: "Delete backup files - use git for version control",
|
|
114
|
+
excludePathPatterns: [],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
pattern: /-new\.(ts|tsx|js|jsx|mjs)$/i,
|
|
118
|
+
name: "new-suffix",
|
|
119
|
+
description: "New suffix indicates incomplete migration",
|
|
120
|
+
severity: "error",
|
|
121
|
+
suggestion: "Complete migration: rename to canonical name",
|
|
122
|
+
excludePathPatterns: [],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
pattern: /-legacy\.(ts|tsx|js|jsx|mjs)$/i,
|
|
126
|
+
name: "legacy-suffix",
|
|
127
|
+
description: "Legacy suffix indicates technical debt",
|
|
128
|
+
severity: "error",
|
|
129
|
+
suggestion: "Migrate away from legacy code and delete",
|
|
130
|
+
excludePathPatterns: [],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
pattern: /-deprecated\.(ts|tsx|js|jsx|mjs)$/i,
|
|
134
|
+
name: "deprecated-suffix",
|
|
135
|
+
description: "Deprecated suffix indicates dead code",
|
|
136
|
+
severity: "error",
|
|
137
|
+
suggestion: "Delete deprecated files",
|
|
138
|
+
excludePathPatterns: [/preflights?\//, /validation\//],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
pattern: /-temp\.(ts|tsx|js|jsx|mjs)$/i,
|
|
142
|
+
name: "temp-suffix",
|
|
143
|
+
description: "Temp suffix indicates temporary file",
|
|
144
|
+
severity: "error",
|
|
145
|
+
suggestion: "Delete temporary files",
|
|
146
|
+
excludePathPatterns: [],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
pattern: /\.bak\.(ts|tsx|js|jsx|mjs)$/i,
|
|
150
|
+
name: "bak-extension",
|
|
151
|
+
description: "Backup extension indicates temporary file",
|
|
152
|
+
severity: "error",
|
|
153
|
+
suggestion: "Delete .bak files - use git for version control",
|
|
154
|
+
excludePathPatterns: [],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pattern: /copy\.(ts|tsx|js|jsx|mjs)$/i,
|
|
158
|
+
name: "copy-suffix",
|
|
159
|
+
description: "Copy suffix indicates duplicate file",
|
|
160
|
+
severity: "error",
|
|
161
|
+
suggestion: "Delete copy files - consolidate into single source",
|
|
162
|
+
excludePathPatterns: [],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: /-wip\.(ts|tsx|js|jsx|mjs)$/i,
|
|
166
|
+
name: "wip-suffix",
|
|
167
|
+
description: "WIP suffix indicates incomplete work",
|
|
168
|
+
severity: "error",
|
|
169
|
+
suggestion: "Complete the work and rename to canonical name",
|
|
170
|
+
excludePathPatterns: [],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
pattern: /-draft\.(ts|tsx|js|jsx|mjs)$/i,
|
|
174
|
+
name: "draft-suffix",
|
|
175
|
+
description: "Draft suffix indicates incomplete work",
|
|
176
|
+
severity: "error",
|
|
177
|
+
suggestion: "Finalize and rename to canonical name",
|
|
178
|
+
excludePathPatterns: [],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
pattern: /-final\.(ts|tsx|js|jsx|mjs)$/i,
|
|
182
|
+
name: "final-suffix",
|
|
183
|
+
description: "Final suffix indicates version confusion",
|
|
184
|
+
severity: "error",
|
|
185
|
+
suggestion: "Rename to canonical name - there should only be one version",
|
|
186
|
+
excludePathPatterns: [],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
pattern: /-updated\.(ts|tsx|js|jsx|mjs)$/i,
|
|
190
|
+
name: "updated-suffix",
|
|
191
|
+
description: "Updated suffix indicates version confusion",
|
|
192
|
+
severity: "error",
|
|
193
|
+
suggestion: "Rename to canonical name and delete old version",
|
|
194
|
+
excludePathPatterns: [],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
pattern: /-fixed\.(ts|tsx|js|jsx|mjs)$/i,
|
|
198
|
+
name: "fixed-suffix",
|
|
199
|
+
description: "Fixed suffix indicates version confusion",
|
|
200
|
+
severity: "error",
|
|
201
|
+
suggestion: "Rename to canonical name and delete broken version",
|
|
202
|
+
excludePathPatterns: [],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
pattern: /-original\.(ts|tsx|js|jsx|mjs)$/i,
|
|
206
|
+
name: "original-suffix",
|
|
207
|
+
description: "Original suffix indicates version confusion",
|
|
208
|
+
severity: "error",
|
|
209
|
+
suggestion: "Delete original if no longer needed, or consolidate",
|
|
210
|
+
excludePathPatterns: [],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
pattern: /-refactored\.(ts|tsx|js|jsx|mjs)$/i,
|
|
214
|
+
name: "refactored-suffix",
|
|
215
|
+
description: "Refactored suffix indicates incomplete migration",
|
|
216
|
+
severity: "error",
|
|
217
|
+
suggestion: "Complete refactor: rename to canonical name",
|
|
218
|
+
excludePathPatterns: [],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
pattern: /-test\.(ts|tsx|js|jsx|mjs)$/i,
|
|
222
|
+
name: "test-suffix-wrong-location",
|
|
223
|
+
description: "Test file outside tests directory",
|
|
224
|
+
severity: "warning",
|
|
225
|
+
suggestion: "Move to tests/ directory or rename to .test.ts",
|
|
226
|
+
// Only flag if NOT in tests directory and NOT a proper .test.ts file
|
|
227
|
+
excludePathPatterns: [/tests\//, /\.test\.(ts|tsx|js|jsx)$/, /\.spec\.(ts|tsx|js|jsx)$/],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
// Files that are legitimately named with patterns that look like anti-patterns
|
|
231
|
+
// These are tools/utilities, not temporary files
|
|
232
|
+
legitimateExceptions: new Set([
|
|
233
|
+
"safe-backup.ts",
|
|
234
|
+
"restore-backup.ts",
|
|
235
|
+
"deploy-with-backup.ts",
|
|
236
|
+
"convert-legacy-backup.ts",
|
|
237
|
+
"mandatory-backup.ts",
|
|
238
|
+
]),
|
|
239
|
+
// Anti-patterns in directory/route segments
|
|
240
|
+
// NOTE: /new/ is a valid Next.js pattern for "create new" pages - not flagged
|
|
241
|
+
// NOTE: /test/ in app/api/ is valid for dev/debug endpoints - not flagged
|
|
242
|
+
routeSegmentAntiPatterns: [
|
|
243
|
+
{
|
|
244
|
+
pattern: /\/old\//,
|
|
245
|
+
name: "old-route-segment",
|
|
246
|
+
description: "'/old/' route segment indicates deprecated route",
|
|
247
|
+
severity: "error",
|
|
248
|
+
suggestion: "Remove old routes or redirect to new ones",
|
|
249
|
+
appOnly: false,
|
|
250
|
+
context: undefined,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
pattern: /\/temp\//,
|
|
254
|
+
name: "temp-route-segment",
|
|
255
|
+
description: "'/temp/' route segment indicates temporary code",
|
|
256
|
+
severity: "error",
|
|
257
|
+
suggestion: "Remove temporary routes",
|
|
258
|
+
appOnly: false,
|
|
259
|
+
context: undefined,
|
|
260
|
+
},
|
|
261
|
+
// NOTE: /test/ in app/api/ is valid for dev/debug API endpoints - removed from checks
|
|
262
|
+
],
|
|
263
|
+
// Directory naming rules
|
|
264
|
+
directoryNamingRules: {
|
|
265
|
+
// Directories that should be kebab-case
|
|
266
|
+
kebabCaseDirs: ["lib", "scripts", "types", "tests"],
|
|
267
|
+
// Directories that should be PascalCase (none by default - components use kebab-case folders)
|
|
268
|
+
pascalCaseDirs: [],
|
|
269
|
+
// Directories that can have any casing (Next.js route segments)
|
|
270
|
+
anyCase: ["app"],
|
|
271
|
+
// Reserved directory names that are always valid
|
|
272
|
+
reserved: new Set([
|
|
273
|
+
"_components",
|
|
274
|
+
"_hooks",
|
|
275
|
+
"_utils",
|
|
276
|
+
"_lib",
|
|
277
|
+
"__mocks__",
|
|
278
|
+
"__tests__",
|
|
279
|
+
"node_modules",
|
|
280
|
+
".next",
|
|
281
|
+
".git",
|
|
282
|
+
"public",
|
|
283
|
+
"prisma",
|
|
284
|
+
]),
|
|
285
|
+
},
|
|
286
|
+
// Duplicate detection settings
|
|
287
|
+
duplicateDetection: {
|
|
288
|
+
// Directories where duplicates are expected (e.g., packages with same structure)
|
|
289
|
+
allowedDuplicatePaths: [
|
|
290
|
+
/packages\//,
|
|
291
|
+
/node_modules\//,
|
|
292
|
+
/scripts\/archived\//,
|
|
293
|
+
/scripts\/active\//,
|
|
294
|
+
],
|
|
295
|
+
// Files that are expected to have duplicates across different contexts
|
|
296
|
+
// This includes: Next.js conventions, common utility names, step components, etc.
|
|
297
|
+
allowedDuplicateNames: new Set([
|
|
298
|
+
// Next.js App Router conventions
|
|
299
|
+
"index.ts",
|
|
300
|
+
"index.tsx",
|
|
301
|
+
"types.ts",
|
|
302
|
+
"constants.ts",
|
|
303
|
+
"utils.ts",
|
|
304
|
+
"utils.tsx",
|
|
305
|
+
"helpers.ts",
|
|
306
|
+
"page.tsx",
|
|
307
|
+
"layout.tsx",
|
|
308
|
+
"loading.tsx",
|
|
309
|
+
"error.tsx",
|
|
310
|
+
"route.ts",
|
|
311
|
+
"not-found.tsx",
|
|
312
|
+
// Common component patterns (same name in different feature areas)
|
|
313
|
+
"validation.ts",
|
|
314
|
+
"hooks.ts",
|
|
315
|
+
// Wizard/stepper patterns (each integration has its own steps)
|
|
316
|
+
"ReviewStep.tsx",
|
|
317
|
+
"ProductTypeStep.tsx",
|
|
318
|
+
"ProductSelectionStep.tsx",
|
|
319
|
+
"FieldMappingStep.tsx",
|
|
320
|
+
"ConfigurationStep.tsx",
|
|
321
|
+
// Page client components (each route has its own client)
|
|
322
|
+
"YearPageClient.tsx",
|
|
323
|
+
"BrandPageClient.tsx",
|
|
324
|
+
"FeaturedPageClient.tsx",
|
|
325
|
+
"SettingsClient.tsx",
|
|
326
|
+
"OrdersClient.tsx",
|
|
327
|
+
"ListingsPageClient.tsx",
|
|
328
|
+
// Common UI component names in different contexts
|
|
329
|
+
"StatCard.tsx",
|
|
330
|
+
"SearchBar.tsx",
|
|
331
|
+
"SearchResults.tsx",
|
|
332
|
+
"FormField.tsx",
|
|
333
|
+
"FormSection.tsx",
|
|
334
|
+
"FormBuilder.tsx",
|
|
335
|
+
"Toast.tsx",
|
|
336
|
+
"ErrorBoundary.tsx",
|
|
337
|
+
"PaginationControls.tsx",
|
|
338
|
+
"BottomPagination.tsx",
|
|
339
|
+
"BulkActionBar.tsx",
|
|
340
|
+
"ImageViewerDialog.tsx",
|
|
341
|
+
"DeleteCardDialog.tsx",
|
|
342
|
+
"ConfidenceIndicator.tsx",
|
|
343
|
+
"CardActions.tsx",
|
|
344
|
+
"TrustBadges.tsx",
|
|
345
|
+
// Feature-specific components that exist in multiple feature areas
|
|
346
|
+
"ListingFilters.tsx",
|
|
347
|
+
"ListingTableRow.tsx",
|
|
348
|
+
"CategorySelector.tsx",
|
|
349
|
+
"ImageUploadSection.tsx",
|
|
350
|
+
"CardDetailsSection.tsx",
|
|
351
|
+
"PhysicalPropertiesSection.tsx",
|
|
352
|
+
"CategorizationSection.tsx",
|
|
353
|
+
"TemplateIndicator.tsx",
|
|
354
|
+
"OnboardingWizard.tsx",
|
|
355
|
+
"EnhancedListingCard.tsx",
|
|
356
|
+
"RecognitionMethodBadge.tsx",
|
|
357
|
+
"DatabaseSuggestions.tsx",
|
|
358
|
+
"CompactFilterContainer.tsx",
|
|
359
|
+
"SmartDropdown.tsx",
|
|
360
|
+
"EnhancedDataTable.tsx",
|
|
361
|
+
"FieldMappingTable.tsx",
|
|
362
|
+
"WooCommerceWizard.tsx",
|
|
363
|
+
"UnifiedProductCard.tsx",
|
|
364
|
+
"UnifiedDialog.tsx",
|
|
365
|
+
"UnifiedHeaderBadge.tsx",
|
|
366
|
+
"BrandManagement.tsx",
|
|
367
|
+
"FocusReviewMode.tsx",
|
|
368
|
+
"PairedCardSlots.tsx",
|
|
369
|
+
"SuggestionToast.tsx",
|
|
370
|
+
// Hooks that exist in multiple contexts
|
|
371
|
+
"useMediaQuery.ts",
|
|
372
|
+
"useSwipeGesture.ts",
|
|
373
|
+
"useListingActions.ts",
|
|
374
|
+
"useDatabaseSuggestions.ts",
|
|
375
|
+
"useCardRecognition.ts",
|
|
376
|
+
"useCardEditing.ts",
|
|
377
|
+
"useWooCommerceConfig.ts",
|
|
378
|
+
"useProductSelection.ts",
|
|
379
|
+
"useCSVImport.ts",
|
|
380
|
+
// Lib files that may have context-specific versions
|
|
381
|
+
"woocommerce.ts",
|
|
382
|
+
"vision-service.ts",
|
|
383
|
+
"seo.ts",
|
|
384
|
+
"reference-data.ts",
|
|
385
|
+
"r2-storage.ts",
|
|
386
|
+
"image-optimization.ts",
|
|
387
|
+
"image-analytics.ts",
|
|
388
|
+
"image-processor.ts",
|
|
389
|
+
"error-handler.ts",
|
|
390
|
+
"categories.ts",
|
|
391
|
+
"card-validation.ts",
|
|
392
|
+
"auth.ts",
|
|
393
|
+
"csv-field-definitions.ts",
|
|
394
|
+
// Script files that may exist in different workflow contexts
|
|
395
|
+
"doctor.mjs",
|
|
396
|
+
"provision-snapshot.ts",
|
|
397
|
+
"create-storage-state.ts",
|
|
398
|
+
"maintenance.ts",
|
|
399
|
+
"fix-nextrequest-imports.ts",
|
|
400
|
+
"log-message-casing.mjs",
|
|
401
|
+
"check-ui-uniformity.ts",
|
|
402
|
+
// Workflow files (each workflow category has similar structure)
|
|
403
|
+
"ui-quality.ts",
|
|
404
|
+
"supercatch.ts",
|
|
405
|
+
"security.ts",
|
|
406
|
+
"performance.ts",
|
|
407
|
+
"images.ts",
|
|
408
|
+
"development.ts",
|
|
409
|
+
"critical.ts",
|
|
410
|
+
"database.ts",
|
|
411
|
+
]),
|
|
412
|
+
},
|
|
413
|
+
// Allowlist file path (relative to project root)
|
|
414
|
+
allowlistPath: ".file-hygiene-allowlist.json",
|
|
415
|
+
};
|
|
416
|
+
// VALIDATOR CLASS
|
|
417
|
+
class FileHygieneValidator {
|
|
418
|
+
violations = [];
|
|
419
|
+
warnings = [];
|
|
420
|
+
filesChecked = 0;
|
|
421
|
+
allowlist;
|
|
422
|
+
verbose;
|
|
423
|
+
warningOnly;
|
|
424
|
+
fixPreview;
|
|
425
|
+
addToAllowlist;
|
|
426
|
+
checkDuplicates;
|
|
427
|
+
checkExports;
|
|
428
|
+
constructor(options = {}) {
|
|
429
|
+
this.verbose = options.verbose || false;
|
|
430
|
+
this.warningOnly = options.warningOnly || false;
|
|
431
|
+
this.fixPreview = options.fixPreview || false;
|
|
432
|
+
this.addToAllowlist = options.addToAllowlist || null;
|
|
433
|
+
this.checkDuplicates = options.checkDuplicates ?? true; // Enable by default
|
|
434
|
+
this.checkExports = options.checkExports ?? false; // Opt-in (slower)
|
|
435
|
+
this.allowlist = this.loadAllowlist();
|
|
436
|
+
}
|
|
437
|
+
loadAllowlist() {
|
|
438
|
+
const allowlistPath = path.join(process.cwd(), CONFIG.allowlistPath);
|
|
439
|
+
if (fs.existsSync(allowlistPath)) {
|
|
440
|
+
try {
|
|
441
|
+
return JSON.parse(fs.readFileSync(allowlistPath, "utf-8"));
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return { files: [], patterns: [], lastUpdated: new Date().toISOString() };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { files: [], patterns: [], lastUpdated: new Date().toISOString() };
|
|
448
|
+
}
|
|
449
|
+
saveAllowlist() {
|
|
450
|
+
const allowlistPath = path.join(process.cwd(), CONFIG.allowlistPath);
|
|
451
|
+
this.allowlist.lastUpdated = new Date().toISOString();
|
|
452
|
+
fs.writeFileSync(allowlistPath, JSON.stringify(this.allowlist, null, 2));
|
|
453
|
+
}
|
|
454
|
+
isAllowlisted(filePath) {
|
|
455
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
456
|
+
// Check exact file match
|
|
457
|
+
if (this.allowlist.files.includes(normalized)) {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
// Check pattern match
|
|
461
|
+
for (const pattern of this.allowlist.patterns) {
|
|
462
|
+
const regex = new RegExp(pattern);
|
|
463
|
+
if (regex.test(normalized)) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
async validate() {
|
|
470
|
+
console.log("\n" + (0, console_chars_1.createDivider)(80, "double"));
|
|
471
|
+
console.log(" FILE HYGIENE VALIDATION (BLOCKING)");
|
|
472
|
+
console.log((0, console_chars_1.createDivider)(80, "double"));
|
|
473
|
+
// Handle add-to-allowlist mode
|
|
474
|
+
if (this.addToAllowlist) {
|
|
475
|
+
return this.handleAddToAllowlist(this.addToAllowlist);
|
|
476
|
+
}
|
|
477
|
+
// Collect all files first for cross-file checks
|
|
478
|
+
const allFiles = [];
|
|
479
|
+
// Scan all directories
|
|
480
|
+
for (const dir of CONFIG.includeDirs) {
|
|
481
|
+
const files = await this.scanDirectory(dir);
|
|
482
|
+
allFiles.push(...files);
|
|
483
|
+
}
|
|
484
|
+
// Run additional checks if enabled
|
|
485
|
+
if (this.checkDuplicates) {
|
|
486
|
+
this.checkDuplicateFilenames(allFiles);
|
|
487
|
+
this.checkCaseCollisions(allFiles);
|
|
488
|
+
}
|
|
489
|
+
if (this.checkExports) {
|
|
490
|
+
await this.checkExportNameMatch(allFiles);
|
|
491
|
+
}
|
|
492
|
+
// Always check directory naming
|
|
493
|
+
this.checkDirectoryNaming(allFiles);
|
|
494
|
+
// Report results
|
|
495
|
+
this.printResults();
|
|
496
|
+
// Write findings for CI
|
|
497
|
+
if (this.violations.length > 0 || this.warnings.length > 0) {
|
|
498
|
+
const findings = [
|
|
499
|
+
...this.violations.map((v) => ({
|
|
500
|
+
message: `${v.file}: ${v.description}`,
|
|
501
|
+
severity: "error",
|
|
502
|
+
file: v.file,
|
|
503
|
+
})),
|
|
504
|
+
...this.warnings.map((v) => ({
|
|
505
|
+
message: `${v.file}: ${v.description}`,
|
|
506
|
+
severity: "warning",
|
|
507
|
+
file: v.file,
|
|
508
|
+
})),
|
|
509
|
+
];
|
|
510
|
+
(0, findings_writer_1.writeFindings)(findings);
|
|
511
|
+
}
|
|
512
|
+
// Determine exit status
|
|
513
|
+
const hasErrors = this.violations.filter((v) => v.severity === "error").length > 0;
|
|
514
|
+
if (this.warningOnly) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
return !hasErrors;
|
|
518
|
+
}
|
|
519
|
+
async scanDirectory(dir) {
|
|
520
|
+
const pattern = `${dir}/**/*.{ts,tsx,js,jsx,mjs}`;
|
|
521
|
+
const files = await (0, glob_1.glob)(pattern, {
|
|
522
|
+
ignore: CONFIG.excludePatterns,
|
|
523
|
+
nodir: true,
|
|
524
|
+
});
|
|
525
|
+
for (const file of files) {
|
|
526
|
+
this.filesChecked++;
|
|
527
|
+
this.checkFile(file);
|
|
528
|
+
}
|
|
529
|
+
return files;
|
|
530
|
+
}
|
|
531
|
+
checkFile(filePath) {
|
|
532
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
533
|
+
const filename = path.basename(filePath);
|
|
534
|
+
// Skip if allowlisted
|
|
535
|
+
if (this.isAllowlisted(normalized)) {
|
|
536
|
+
if (this.verbose) {
|
|
537
|
+
console.log(` ${console_chars_1.emoji.skip} Skipped (allowlisted): ${normalized}`);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// Skip legitimate exceptions (backup tools, etc.)
|
|
542
|
+
if (CONFIG.legitimateExceptions.has(filename)) {
|
|
543
|
+
if (this.verbose) {
|
|
544
|
+
console.log(` ${console_chars_1.emoji.skip} Skipped (legitimate tool): ${normalized}`);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Check filename anti-patterns
|
|
549
|
+
for (const antiPattern of CONFIG.filenameAntiPatterns) {
|
|
550
|
+
if (antiPattern.pattern.test(filename)) {
|
|
551
|
+
// Check if this file path should be excluded for this pattern
|
|
552
|
+
const shouldExclude = antiPattern.excludePathPatterns.some((excludePattern) => excludePattern.test(normalized));
|
|
553
|
+
if (shouldExclude) {
|
|
554
|
+
if (this.verbose) {
|
|
555
|
+
console.log(` ${console_chars_1.emoji.skip} Skipped (path exclusion): ${normalized}`);
|
|
556
|
+
}
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const violation = {
|
|
560
|
+
file: normalized,
|
|
561
|
+
type: "filename",
|
|
562
|
+
patternName: antiPattern.name,
|
|
563
|
+
description: antiPattern.description,
|
|
564
|
+
severity: antiPattern.severity,
|
|
565
|
+
suggestion: antiPattern.suggestion,
|
|
566
|
+
};
|
|
567
|
+
if (antiPattern.severity === "error") {
|
|
568
|
+
this.violations.push(violation);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
this.warnings.push(violation);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Check route segment anti-patterns
|
|
576
|
+
for (const antiPattern of CONFIG.routeSegmentAntiPatterns) {
|
|
577
|
+
// Skip non-app patterns if appOnly is set
|
|
578
|
+
if (antiPattern.appOnly && !normalized.startsWith("app/")) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (antiPattern.pattern.test(normalized)) {
|
|
582
|
+
const violation = {
|
|
583
|
+
file: normalized,
|
|
584
|
+
type: "route-segment",
|
|
585
|
+
patternName: antiPattern.name,
|
|
586
|
+
description: antiPattern.description,
|
|
587
|
+
severity: antiPattern.severity,
|
|
588
|
+
suggestion: antiPattern.suggestion,
|
|
589
|
+
context: antiPattern.context,
|
|
590
|
+
};
|
|
591
|
+
if (antiPattern.severity === "error") {
|
|
592
|
+
this.violations.push(violation);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
this.warnings.push(violation);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
handleAddToAllowlist(filePath) {
|
|
601
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
602
|
+
if (!this.allowlist.files.includes(normalized)) {
|
|
603
|
+
this.allowlist.files.push(normalized);
|
|
604
|
+
this.saveAllowlist();
|
|
605
|
+
console.log(`${console_chars_1.emoji.success} Added to allowlist: ${normalized}`);
|
|
606
|
+
console.log(`${console_chars_1.emoji.hint} Allowlist saved to: ${CONFIG.allowlistPath}`);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.log(`${console_chars_1.emoji.info} Already in allowlist: ${normalized}`);
|
|
610
|
+
}
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
printResults() {
|
|
614
|
+
console.log(`\n${console_chars_1.emoji.search} Scanned ${this.filesChecked} files\n`);
|
|
615
|
+
// Print errors
|
|
616
|
+
if (this.violations.length > 0) {
|
|
617
|
+
console.log((0, console_chars_1.createDivider)(60, "heavy"));
|
|
618
|
+
console.log(`${console_chars_1.emoji.error} ERRORS (${this.violations.length})`);
|
|
619
|
+
console.log((0, console_chars_1.createDivider)(60, "heavy"));
|
|
620
|
+
// Group by pattern type
|
|
621
|
+
const byPattern = new Map();
|
|
622
|
+
for (const v of this.violations) {
|
|
623
|
+
const existing = byPattern.get(v.patternName) || [];
|
|
624
|
+
existing.push(v);
|
|
625
|
+
byPattern.set(v.patternName, existing);
|
|
626
|
+
}
|
|
627
|
+
for (const [patternName, violations] of byPattern) {
|
|
628
|
+
console.log(`\n${console_chars_1.chars.bullet} ${patternName} (${violations.length} files):`);
|
|
629
|
+
for (const v of violations) {
|
|
630
|
+
console.log(` ${console_chars_1.chars.cross} ${v.file}`);
|
|
631
|
+
if (this.verbose) {
|
|
632
|
+
console.log(` ${console_chars_1.chars.arrow} ${v.description}`);
|
|
633
|
+
console.log(` ${console_chars_1.emoji.hint} ${v.suggestion}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Print warnings
|
|
639
|
+
if (this.warnings.length > 0) {
|
|
640
|
+
console.log(`\n${(0, console_chars_1.createDivider)(60, "light")}`);
|
|
641
|
+
console.log(`${console_chars_1.emoji.warning} WARNINGS (${this.warnings.length})`);
|
|
642
|
+
console.log((0, console_chars_1.createDivider)(60, "light"));
|
|
643
|
+
for (const v of this.warnings) {
|
|
644
|
+
console.log(` ${console_chars_1.chars.warning} ${v.file}`);
|
|
645
|
+
if (this.verbose || v.context) {
|
|
646
|
+
console.log(` ${console_chars_1.chars.arrow} ${v.description}`);
|
|
647
|
+
if (v.context) {
|
|
648
|
+
console.log(` ${console_chars_1.chars.info} ${v.context}`);
|
|
649
|
+
}
|
|
650
|
+
console.log(` ${console_chars_1.emoji.hint} ${v.suggestion}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Print fix preview if requested
|
|
655
|
+
if (this.fixPreview && this.violations.length > 0) {
|
|
656
|
+
this.printMigrationSuggestions();
|
|
657
|
+
}
|
|
658
|
+
// Summary
|
|
659
|
+
console.log("\n" + (0, console_chars_1.createDivider)(60, "double"));
|
|
660
|
+
if (this.violations.length === 0 && this.warnings.length === 0) {
|
|
661
|
+
console.log(`${console_chars_1.emoji.success} FILE HYGIENE: PASSED`);
|
|
662
|
+
}
|
|
663
|
+
else if (this.violations.length === 0) {
|
|
664
|
+
console.log(`${console_chars_1.emoji.warning} FILE HYGIENE: PASSED WITH WARNINGS`);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
console.log(`${console_chars_1.emoji.error} FILE HYGIENE: FAILED`);
|
|
668
|
+
console.log(`\n${console_chars_1.emoji.hint} To allowlist a file (with justification):`);
|
|
669
|
+
console.log(` pnpm preflight:file-hygiene --add-allowlist <file>`);
|
|
670
|
+
console.log(`\n${console_chars_1.emoji.hint} To see migration suggestions:`);
|
|
671
|
+
console.log(` pnpm preflight:file-hygiene --fix-preview`);
|
|
672
|
+
}
|
|
673
|
+
console.log((0, console_chars_1.createDivider)(60, "double"));
|
|
674
|
+
}
|
|
675
|
+
printMigrationSuggestions() {
|
|
676
|
+
console.log(`\n${(0, console_chars_1.createDivider)(60, "heavy")}`);
|
|
677
|
+
console.log(`${console_chars_1.emoji.wrench} MIGRATION SUGGESTIONS`);
|
|
678
|
+
console.log((0, console_chars_1.createDivider)(60, "heavy"));
|
|
679
|
+
for (const v of this.violations) {
|
|
680
|
+
if (v.type === "filename" && v.patternName === "version-suffix") {
|
|
681
|
+
const suggestion = this.generateMigrationSuggestion(v.file);
|
|
682
|
+
if (suggestion) {
|
|
683
|
+
console.log(`\n${console_chars_1.chars.bullet} ${v.file}`);
|
|
684
|
+
console.log(` ${console_chars_1.chars.arrow} Rename to: ${suggestion.to}`);
|
|
685
|
+
console.log(` ${console_chars_1.chars.info} Commands to run:`);
|
|
686
|
+
for (const cmd of suggestion.commands) {
|
|
687
|
+
console.log(` $ ${cmd}`);
|
|
688
|
+
}
|
|
689
|
+
if (suggestion.affectedImports.length > 0) {
|
|
690
|
+
console.log(` ${console_chars_1.chars.warning} Files with imports to update:`);
|
|
691
|
+
for (const imp of suggestion.affectedImports.slice(0, 5)) {
|
|
692
|
+
console.log(` - ${imp}`);
|
|
693
|
+
}
|
|
694
|
+
if (suggestion.affectedImports.length > 5) {
|
|
695
|
+
console.log(` ... and ${suggestion.affectedImports.length - 5} more`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Check for duplicate filenames across different directories
|
|
704
|
+
* (excluding expected duplicates like index.ts, page.tsx, etc.)
|
|
705
|
+
*/
|
|
706
|
+
checkDuplicateFilenames(allFiles) {
|
|
707
|
+
const filenameMap = new Map();
|
|
708
|
+
for (const file of allFiles) {
|
|
709
|
+
const normalized = file.replace(/\\/g, "/");
|
|
710
|
+
const filename = path.basename(file);
|
|
711
|
+
// Skip allowed duplicates
|
|
712
|
+
if (CONFIG.duplicateDetection.allowedDuplicateNames.has(filename)) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
// Skip files in allowed duplicate paths
|
|
716
|
+
if (CONFIG.duplicateDetection.allowedDuplicatePaths.some((p) => p.test(normalized))) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const existing = filenameMap.get(filename) || [];
|
|
720
|
+
existing.push(normalized);
|
|
721
|
+
filenameMap.set(filename, existing);
|
|
722
|
+
}
|
|
723
|
+
// Report duplicates
|
|
724
|
+
for (const [filename, paths] of filenameMap) {
|
|
725
|
+
if (paths.length > 1) {
|
|
726
|
+
// Check if they're in the same logical area (both in components, both in lib, etc.)
|
|
727
|
+
const rootDirs = new Set(paths.map((p) => p.split("/")[0]));
|
|
728
|
+
// Only flag if duplicates are in different root directories (more likely to be confusing)
|
|
729
|
+
if (rootDirs.size > 1 || this.verbose) {
|
|
730
|
+
this.warnings.push({
|
|
731
|
+
file: paths[0],
|
|
732
|
+
type: "duplicate",
|
|
733
|
+
patternName: "duplicate-filename",
|
|
734
|
+
description: `Duplicate filename '${filename}' found in ${paths.length} locations`,
|
|
735
|
+
severity: "warning",
|
|
736
|
+
suggestion: "Consider renaming to be more specific, or consolidate into shared location",
|
|
737
|
+
relatedFiles: paths.slice(1),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Check for case collisions (files that differ only in casing)
|
|
745
|
+
* This can cause issues on case-insensitive file systems (Windows, macOS)
|
|
746
|
+
*/
|
|
747
|
+
checkCaseCollisions(allFiles) {
|
|
748
|
+
const lowercaseMap = new Map();
|
|
749
|
+
for (const file of allFiles) {
|
|
750
|
+
const normalized = file.replace(/\\/g, "/");
|
|
751
|
+
const lowercase = normalized.toLowerCase();
|
|
752
|
+
const existing = lowercaseMap.get(lowercase) || [];
|
|
753
|
+
existing.push(normalized);
|
|
754
|
+
lowercaseMap.set(lowercase, existing);
|
|
755
|
+
}
|
|
756
|
+
// Report case collisions
|
|
757
|
+
for (const [, paths] of lowercaseMap) {
|
|
758
|
+
if (paths.length > 1) {
|
|
759
|
+
// Check if they actually differ in casing (not just duplicates)
|
|
760
|
+
const uniquePaths = new Set(paths);
|
|
761
|
+
if (uniquePaths.size > 1) {
|
|
762
|
+
this.violations.push({
|
|
763
|
+
file: paths[0],
|
|
764
|
+
type: "case-collision",
|
|
765
|
+
patternName: "case-collision",
|
|
766
|
+
description: `Case collision: files differ only in casing`,
|
|
767
|
+
severity: "error",
|
|
768
|
+
suggestion: "Rename files to have distinct names - case collisions cause issues on Windows/macOS",
|
|
769
|
+
relatedFiles: paths.slice(1),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Check that component files export a component matching their filename
|
|
777
|
+
* e.g., UserCard.tsx should export UserCard, not ProfileCard
|
|
778
|
+
*/
|
|
779
|
+
async checkExportNameMatch(allFiles) {
|
|
780
|
+
const componentFiles = allFiles.filter((f) => {
|
|
781
|
+
const normalized = f.replace(/\\/g, "/");
|
|
782
|
+
const filename = path.basename(f);
|
|
783
|
+
// Only check .tsx files in components/ or app/ that are PascalCase
|
|
784
|
+
return (/\.(tsx)$/.test(filename) &&
|
|
785
|
+
/^[A-Z]/.test(filename) &&
|
|
786
|
+
(normalized.startsWith("components/") || normalized.includes("/_components/")));
|
|
787
|
+
});
|
|
788
|
+
for (const file of componentFiles) {
|
|
789
|
+
const filename = path.basename(file, path.extname(file));
|
|
790
|
+
// Skip files with valid suffixes
|
|
791
|
+
if (/\.(stories|test|spec|examples)$/.test(filename)) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
796
|
+
// Look for default export
|
|
797
|
+
const defaultExportMatch = content.match(/export\s+default\s+(?:function\s+)?(\w+)|export\s*{\s*(\w+)\s+as\s+default\s*}/);
|
|
798
|
+
if (defaultExportMatch) {
|
|
799
|
+
const exportedName = defaultExportMatch[1] || defaultExportMatch[2];
|
|
800
|
+
// Check if exported name matches filename (allowing for minor variations)
|
|
801
|
+
if (exportedName && exportedName !== filename) {
|
|
802
|
+
// Allow Client suffix mismatch (e.g., UserCard.tsx exports UserCardClient)
|
|
803
|
+
if (exportedName === `${filename}Client` || filename === `${exportedName}Client`) {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
this.warnings.push({
|
|
807
|
+
file: file.replace(/\\/g, "/"),
|
|
808
|
+
type: "export-mismatch",
|
|
809
|
+
patternName: "export-name-mismatch",
|
|
810
|
+
description: `File '${filename}.tsx' exports '${exportedName}' - names should match`,
|
|
811
|
+
severity: "warning",
|
|
812
|
+
suggestion: `Rename file to '${exportedName}.tsx' or rename export to '${filename}'`,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
// Skip files that can't be read
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Check directory naming conventions
|
|
824
|
+
*/
|
|
825
|
+
checkDirectoryNaming(allFiles) {
|
|
826
|
+
const checkedDirs = new Set();
|
|
827
|
+
for (const file of allFiles) {
|
|
828
|
+
const normalized = file.replace(/\\/g, "/");
|
|
829
|
+
const parts = normalized.split("/");
|
|
830
|
+
// Check each directory in the path
|
|
831
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
832
|
+
const dirName = parts[i];
|
|
833
|
+
const fullDirPath = parts.slice(0, i + 1).join("/");
|
|
834
|
+
// Skip if already checked
|
|
835
|
+
if (checkedDirs.has(fullDirPath))
|
|
836
|
+
continue;
|
|
837
|
+
checkedDirs.add(fullDirPath);
|
|
838
|
+
// Skip reserved directories
|
|
839
|
+
if (CONFIG.directoryNamingRules.reserved.has(dirName))
|
|
840
|
+
continue;
|
|
841
|
+
// Skip dynamic route segments [id], [...slug], etc.
|
|
842
|
+
if (/^\[.*\]$/.test(dirName))
|
|
843
|
+
continue;
|
|
844
|
+
// Skip private folders starting with _
|
|
845
|
+
if (dirName.startsWith("_"))
|
|
846
|
+
continue;
|
|
847
|
+
// Skip route groups (parentheses)
|
|
848
|
+
if (/^\(.*\)$/.test(dirName))
|
|
849
|
+
continue;
|
|
850
|
+
// Determine which convention applies
|
|
851
|
+
const rootDir = parts[0];
|
|
852
|
+
// Skip app directory (Next.js routes can be any casing)
|
|
853
|
+
if (rootDir === "app")
|
|
854
|
+
continue;
|
|
855
|
+
// Check for problematic patterns in directory names
|
|
856
|
+
if (/-v\d+$/.test(dirName)) {
|
|
857
|
+
this.violations.push({
|
|
858
|
+
file: fullDirPath,
|
|
859
|
+
type: "directory",
|
|
860
|
+
patternName: "directory-version-suffix",
|
|
861
|
+
description: `Directory '${dirName}' has version suffix`,
|
|
862
|
+
severity: "error",
|
|
863
|
+
suggestion: "Rename directory to remove version suffix",
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
if (/-old$|-new$|-backup$|-temp$/.test(dirName)) {
|
|
867
|
+
this.violations.push({
|
|
868
|
+
file: fullDirPath,
|
|
869
|
+
type: "directory",
|
|
870
|
+
patternName: "directory-temp-suffix",
|
|
871
|
+
description: `Directory '${dirName}' has temporary suffix`,
|
|
872
|
+
severity: "error",
|
|
873
|
+
suggestion: "Rename or remove temporary directory",
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
// Check for spaces or special characters
|
|
877
|
+
if (/\s|[^a-zA-Z0-9_\-\[\]\(\)]/.test(dirName)) {
|
|
878
|
+
this.violations.push({
|
|
879
|
+
file: fullDirPath,
|
|
880
|
+
type: "directory",
|
|
881
|
+
patternName: "directory-invalid-chars",
|
|
882
|
+
description: `Directory '${dirName}' contains spaces or special characters`,
|
|
883
|
+
severity: "error",
|
|
884
|
+
suggestion: "Use only alphanumeric characters, hyphens, and underscores",
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
generateMigrationSuggestion(filePath) {
|
|
891
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
892
|
+
const dir = path.dirname(normalized);
|
|
893
|
+
const filename = path.basename(normalized);
|
|
894
|
+
// Remove version suffix
|
|
895
|
+
const newFilename = filename.replace(/-v\d+(\.[^.]+)$/, "$1");
|
|
896
|
+
const newPath = path.join(dir, newFilename).replace(/\\/g, "/");
|
|
897
|
+
// Find files that import this
|
|
898
|
+
const affectedImports = [];
|
|
899
|
+
// This would require scanning imports - simplified for now
|
|
900
|
+
return {
|
|
901
|
+
from: normalized,
|
|
902
|
+
to: newPath,
|
|
903
|
+
affectedImports,
|
|
904
|
+
commands: [
|
|
905
|
+
`git mv "${normalized}" "${newPath}"`,
|
|
906
|
+
`# Update imports in affected files`,
|
|
907
|
+
`pnpm preflight:import-validation --fix`,
|
|
908
|
+
],
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// MAIN
|
|
913
|
+
async function run() {
|
|
914
|
+
const reporter = (0, universal_progress_reporter_1.createUniversalProgressReporter)(exports.name);
|
|
915
|
+
const args = process.argv.slice(2);
|
|
916
|
+
const options = {
|
|
917
|
+
verbose: args.includes("--verbose") || args.includes("-v"),
|
|
918
|
+
warningOnly: args.includes("--warning") || args.includes("-w"),
|
|
919
|
+
fixPreview: args.includes("--fix-preview") || args.includes("--fix"),
|
|
920
|
+
addToAllowlist: args.includes("--add-allowlist")
|
|
921
|
+
? args[args.indexOf("--add-allowlist") + 1]
|
|
922
|
+
: undefined,
|
|
923
|
+
checkDuplicates: !args.includes("--no-duplicates"),
|
|
924
|
+
checkExports: args.includes("--check-exports"),
|
|
925
|
+
};
|
|
926
|
+
const validator = new FileHygieneValidator(options);
|
|
927
|
+
const passed = await validator.validate();
|
|
928
|
+
process.exit(passed ? 0 : 1);
|
|
929
|
+
}
|
|
930
|
+
// Allow direct execution
|
|
931
|
+
if (require.main === module) {
|
|
932
|
+
run().catch(console.error);
|
|
933
|
+
}
|
|
934
|
+
//# sourceMappingURL=file-hygiene-validation.js.map
|