@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.
Files changed (49) hide show
  1. package/dist/checks/auth/session-provider-wrapper.d.ts +47 -0
  2. package/dist/checks/auth/session-provider-wrapper.d.ts.map +1 -0
  3. package/dist/checks/auth/session-provider-wrapper.js +286 -0
  4. package/dist/checks/auth/session-provider-wrapper.js.map +1 -0
  5. package/dist/checks/database/prisma-upsert-safety.d.ts +39 -0
  6. package/dist/checks/database/prisma-upsert-safety.d.ts.map +1 -0
  7. package/dist/checks/database/prisma-upsert-safety.js +220 -0
  8. package/dist/checks/database/prisma-upsert-safety.js.map +1 -0
  9. package/dist/checks/dependencies/dependency-health-monitor.d.ts +49 -0
  10. package/dist/checks/dependencies/dependency-health-monitor.d.ts.map +1 -0
  11. package/dist/checks/dependencies/dependency-health-monitor.js +323 -0
  12. package/dist/checks/dependencies/dependency-health-monitor.js.map +1 -0
  13. package/dist/checks/file-hygiene-validation.d.ts +31 -0
  14. package/dist/checks/file-hygiene-validation.d.ts.map +1 -0
  15. package/dist/checks/file-hygiene-validation.js +934 -0
  16. package/dist/checks/file-hygiene-validation.js.map +1 -0
  17. package/dist/checks/organization/file-cleanup-validation.d.ts +22 -0
  18. package/dist/checks/organization/file-cleanup-validation.d.ts.map +1 -0
  19. package/dist/checks/organization/file-cleanup-validation.js +1121 -0
  20. package/dist/checks/organization/file-cleanup-validation.js.map +1 -0
  21. package/dist/checks/runtime/tailwind-runtime-check.d.ts +36 -0
  22. package/dist/checks/runtime/tailwind-runtime-check.d.ts.map +1 -0
  23. package/dist/checks/runtime/tailwind-runtime-check.js +264 -0
  24. package/dist/checks/runtime/tailwind-runtime-check.js.map +1 -0
  25. package/dist/checks/shipping-integration-validation.d.ts +28 -0
  26. package/dist/checks/shipping-integration-validation.d.ts.map +1 -0
  27. package/dist/checks/shipping-integration-validation.js +409 -0
  28. package/dist/checks/shipping-integration-validation.js.map +1 -0
  29. package/dist/checks/system/layout-constants-sync.d.ts +36 -0
  30. package/dist/checks/system/layout-constants-sync.d.ts.map +1 -0
  31. package/dist/checks/system/layout-constants-sync.js +642 -0
  32. package/dist/checks/system/layout-constants-sync.js.map +1 -0
  33. package/dist/checks/system/preflight-circular-dependency-detector.d.ts +26 -0
  34. package/dist/checks/system/preflight-circular-dependency-detector.d.ts.map +1 -0
  35. package/dist/checks/system/preflight-circular-dependency-detector.js +310 -0
  36. package/dist/checks/system/preflight-circular-dependency-detector.js.map +1 -0
  37. package/dist/checks/system/preflight-execution-benchmarks.d.ts +24 -0
  38. package/dist/checks/system/preflight-execution-benchmarks.d.ts.map +1 -0
  39. package/dist/checks/system/preflight-execution-benchmarks.js +282 -0
  40. package/dist/checks/system/preflight-execution-benchmarks.js.map +1 -0
  41. package/dist/checks/system/preflight-tag-taxonomy-validator.d.ts +27 -0
  42. package/dist/checks/system/preflight-tag-taxonomy-validator.d.ts.map +1 -0
  43. package/dist/checks/system/preflight-tag-taxonomy-validator.js +361 -0
  44. package/dist/checks/system/preflight-tag-taxonomy-validator.js.map +1 -0
  45. package/dist/utils/console-chars.d.ts +16 -0
  46. package/dist/utils/console-chars.d.ts.map +1 -1
  47. package/dist/utils/console-chars.js +10 -0
  48. package/dist/utils/console-chars.js.map +1 -1
  49. 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