predeploy-check 1.0.0 → 1.1.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.
@@ -1,228 +1,240 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const name = 'Case Sensitivity: import path mismatches';
7
-
8
- // File extensions to scan for import statements
9
- const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts']);
10
-
11
- // Directories to skip
12
- const SKIP_DIRS = new Set([
13
- 'node_modules', '.git', '.next', 'dist', 'build', '.vercel',
14
- '.output', 'coverage', '__pycache__', '.cache',
15
- ]);
16
-
17
- /**
18
- * Recursively collect all scannable files.
19
- */
20
- function collectFiles(dir, files = []) {
21
- let entries;
22
- try {
23
- entries = fs.readdirSync(dir, { withFileTypes: true });
24
- } catch {
25
- return files;
26
- }
27
-
28
- for (const entry of entries) {
29
- if (entry.isDirectory()) {
30
- if (!SKIP_DIRS.has(entry.name)) {
31
- collectFiles(path.join(dir, entry.name), files);
32
- }
33
- } else if (entry.isFile() && SCAN_EXTENSIONS.has(path.extname(entry.name))) {
34
- files.push(path.join(dir, entry.name));
35
- }
36
- }
37
-
38
- return files;
39
- }
40
-
41
- /**
42
- * Extract relative import paths from a file's content.
43
- * Returns array of { line, importPath } objects.
44
- */
45
- function extractImports(content) {
46
- const imports = [];
47
- const lines = content.split('\n');
48
-
49
- for (let i = 0; i < lines.length; i++) {
50
- const line = lines[i];
51
-
52
- // Match: import ... from './path' or import ... from "../path"
53
- // Match: require('./path') or require("../path")
54
- // Match: import('./path') dynamic imports
55
- const patterns = [
56
- /(?:from|require|import)\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g,
57
- /from\s+['"](\.[^'"]+)['"]/g,
58
- ];
59
-
60
- for (const pattern of patterns) {
61
- let match;
62
- while ((match = pattern.exec(line)) !== null) {
63
- imports.push({
64
- line: i + 1,
65
- importPath: match[1],
66
- });
67
- }
68
- }
69
- }
70
-
71
- return imports;
72
- }
73
-
74
- /**
75
- * Build a map of directory → Set of actual filenames (preserving case).
76
- */
77
- function buildFilenameCache(projectRoot) {
78
- const cache = new Map();
79
-
80
- function walk(dir) {
81
- let entries;
82
- try {
83
- entries = fs.readdirSync(dir, { withFileTypes: true });
84
- } catch {
85
- return;
86
- }
87
-
88
- const names = new Set();
89
- for (const entry of entries) {
90
- names.add(entry.name);
91
- if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
92
- walk(path.join(dir, entry.name));
93
- }
94
- }
95
- cache.set(dir, names);
96
- }
97
-
98
- walk(projectRoot);
99
- return cache;
100
- }
101
-
102
- /**
103
- * Resolve an import path to an actual file, trying common extensions.
104
- * Returns { resolved, actual } where:
105
- * - resolved is the expected filename from the import
106
- * - actual is the real filename on disk (may differ in case)
107
- * - null if the file doesn't exist at all
108
- */
109
- function resolveImport(importPath, fromFile, filenameCache) {
110
- const fromDir = path.dirname(fromFile);
111
- const resolved = path.resolve(fromDir, importPath);
112
- const dir = path.dirname(resolved);
113
- const basename = path.basename(resolved);
114
-
115
- const dirFiles = filenameCache.get(dir);
116
- if (!dirFiles) return null;
117
-
118
- // Extensions to try if the import has no extension
119
- const ext = path.extname(basename);
120
- const candidates = ext
121
- ? [basename]
122
- : [
123
- `${basename}.js`, `${basename}.ts`, `${basename}.jsx`, `${basename}.tsx`,
124
- `${basename}.mjs`, `${basename}.mts`,
125
- // index files
126
- `${basename}/index.js`, `${basename}/index.ts`,
127
- `${basename}/index.jsx`, `${basename}/index.tsx`,
128
- ];
129
-
130
- for (const candidate of candidates) {
131
- // Handle directory/index patterns
132
- if (candidate.includes('/')) {
133
- const [subDir, indexFile] = candidate.split('/');
134
- const subDirPath = path.join(dir, subDir);
135
- const subFiles = filenameCache.get(subDirPath);
136
-
137
- if (subFiles) {
138
- // Check if directory name itself has a case mismatch
139
- if (dirFiles) {
140
- for (const actual of dirFiles) {
141
- if (actual.toLowerCase() === subDir.toLowerCase() && actual !== subDir) {
142
- return { expected: subDir, actual, type: 'directory' };
143
- }
144
- }
145
- }
146
- // Check index file inside the directory
147
- for (const actual of subFiles) {
148
- if (actual.toLowerCase() === indexFile.toLowerCase() && actual !== indexFile) {
149
- return { expected: indexFile, actual, type: 'file' };
150
- }
151
- }
152
- }
153
- continue;
154
- }
155
-
156
- // Direct file match
157
- for (const actual of dirFiles) {
158
- if (actual.toLowerCase() === candidate.toLowerCase()) {
159
- if (actual !== candidate) {
160
- return { expected: candidate, actual, type: 'file' };
161
- }
162
- // Exact match — no case issue
163
- return null;
164
- }
165
- }
166
- }
167
-
168
- return null; // File not found at all — not a case issue, just a missing file
169
- }
170
-
171
- async function run(projectRoot) {
172
- const files = collectFiles(projectRoot);
173
-
174
- if (files.length === 0) {
175
- return {
176
- status: 'skip',
177
- message: `${name} — no JS/TS files found to scan`,
178
- };
179
- }
180
-
181
- const filenameCache = buildFilenameCache(projectRoot);
182
- const mismatches = [];
183
-
184
- for (const file of files) {
185
- let content;
186
- try {
187
- content = fs.readFileSync(file, 'utf-8');
188
- } catch {
189
- continue;
190
- }
191
-
192
- const imports = extractImports(content);
193
-
194
- for (const imp of imports) {
195
- const result = resolveImport(imp.importPath, file, filenameCache);
196
- if (result) {
197
- const relFile = path.relative(projectRoot, file);
198
- mismatches.push({
199
- file: relFile,
200
- line: imp.line,
201
- importPath: imp.importPath,
202
- expected: result.expected,
203
- actual: result.actual,
204
- });
205
- }
206
- }
207
- }
208
-
209
- if (mismatches.length === 0) {
210
- return {
211
- status: 'pass',
212
- message: `${name} — all ${files.length} files checked, no case mismatches found`,
213
- };
214
- }
215
-
216
- return {
217
- status: 'warn',
218
- message: `${name} — ${mismatches.length} case mismatch(es) found (will fail on Vercel's Linux filesystem)`,
219
- fix: 'Rename the import paths to exactly match the filename casing on disk',
220
- details: mismatches.map((m) => ({
221
- file: m.file,
222
- line: m.line,
223
- message: `Import "${m.importPath}" references "${m.expected}" but file on disk is "${m.actual}"`,
224
- })),
225
- };
226
- }
227
-
228
- module.exports = { name, run };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const name = 'Case Sensitivity: import path mismatches';
7
+
8
+ // File extensions to scan for import statements
9
+ const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts']);
10
+
11
+ // Directories to skip
12
+ const SKIP_DIRS = new Set([
13
+ 'node_modules', '.git', '.next', 'dist', 'build', '.vercel',
14
+ '.output', 'coverage', '__pycache__', '.cache',
15
+ ]);
16
+
17
+ /**
18
+ * Recursively collect all scannable files.
19
+ */
20
+ function collectFiles(dir, files = []) {
21
+ let entries;
22
+ try {
23
+ entries = fs.readdirSync(dir, { withFileTypes: true });
24
+ } catch {
25
+ return files;
26
+ }
27
+
28
+ for (const entry of entries) {
29
+ if (entry.isDirectory()) {
30
+ if (!SKIP_DIRS.has(entry.name)) {
31
+ collectFiles(path.join(dir, entry.name), files);
32
+ }
33
+ } else if (entry.isFile() && SCAN_EXTENSIONS.has(path.extname(entry.name))) {
34
+ files.push(path.join(dir, entry.name));
35
+ }
36
+ }
37
+
38
+ return files;
39
+ }
40
+
41
+ /**
42
+ * Extract relative import paths from a file's content.
43
+ * Returns array of { line, importPath } objects.
44
+ */
45
+ function extractImports(content) {
46
+ const imports = [];
47
+ const lines = content.split('\n');
48
+
49
+ for (let i = 0; i < lines.length; i++) {
50
+ const line = lines[i];
51
+
52
+ // Match: import ... from './path' or import ... from "../path"
53
+ // Match: require('./path') or require("../path")
54
+ // Match: import('./path') dynamic imports
55
+ const patterns = [
56
+ /(?:from|require|import)\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g,
57
+ /from\s+['"](\.[^'"]+)['"]/g,
58
+ ];
59
+
60
+ for (const pattern of patterns) {
61
+ let match;
62
+ while ((match = pattern.exec(line)) !== null) {
63
+ imports.push({
64
+ line: i + 1,
65
+ importPath: match[1],
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ return imports;
72
+ }
73
+
74
+ /**
75
+ * Build a map of directory → Set of actual filenames (preserving case).
76
+ */
77
+ function buildFilenameCache(projectRoot) {
78
+ const cache = new Map();
79
+
80
+ function walk(dir) {
81
+ let entries;
82
+ try {
83
+ entries = fs.readdirSync(dir, { withFileTypes: true });
84
+ } catch {
85
+ return;
86
+ }
87
+
88
+ const names = new Set();
89
+ for (const entry of entries) {
90
+ names.add(entry.name);
91
+ if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
92
+ walk(path.join(dir, entry.name));
93
+ }
94
+ }
95
+ cache.set(dir, names);
96
+ }
97
+
98
+ walk(projectRoot);
99
+ return cache;
100
+ }
101
+
102
+ /**
103
+ * Resolve an import path to an actual file, trying common extensions.
104
+ * Returns { resolved, actual } where:
105
+ * - resolved is the expected filename from the import
106
+ * - actual is the real filename on disk (may differ in case)
107
+ * - null if the file doesn't exist at all
108
+ */
109
+ function resolveImport(importPath, fromFile, filenameCache) {
110
+ const fromDir = path.dirname(fromFile);
111
+ const resolved = path.resolve(fromDir, importPath);
112
+ const dir = path.dirname(resolved);
113
+ const basename = path.basename(resolved);
114
+
115
+ const dirFiles = filenameCache.get(dir);
116
+ if (!dirFiles) return null;
117
+
118
+ // Extensions to try if the import has no extension
119
+ const ext = path.extname(basename);
120
+ const candidates = ext
121
+ ? [basename]
122
+ : [
123
+ `${basename}.js`, `${basename}.ts`, `${basename}.jsx`, `${basename}.tsx`,
124
+ `${basename}.mjs`, `${basename}.mts`,
125
+ // index files
126
+ `${basename}/index.js`, `${basename}/index.ts`,
127
+ `${basename}/index.jsx`, `${basename}/index.tsx`,
128
+ ];
129
+
130
+ for (const candidate of candidates) {
131
+ // Handle directory/index patterns
132
+ if (candidate.includes('/')) {
133
+ const [subDir, indexFile] = candidate.split('/');
134
+
135
+ // Find the directory on disk regardless of the casing used in the
136
+ // import — a mismatched cache-key lookup here would silently skip
137
+ // the whole branch and miss a real case mismatch.
138
+ let actualSubDirName = null;
139
+ if (dirFiles) {
140
+ for (const actual of dirFiles) {
141
+ if (actual.toLowerCase() === subDir.toLowerCase()) {
142
+ actualSubDirName = actual;
143
+ break;
144
+ }
145
+ }
146
+ }
147
+
148
+ if (actualSubDirName === null) {
149
+ continue; // No directory by this name exists at all, in any casing
150
+ }
151
+
152
+ if (actualSubDirName !== subDir) {
153
+ return { expected: subDir, actual: actualSubDirName, type: 'directory' };
154
+ }
155
+
156
+ // Directory casing matched — now check the index file inside it
157
+ const subFiles = filenameCache.get(path.join(dir, actualSubDirName));
158
+ if (subFiles) {
159
+ for (const actual of subFiles) {
160
+ if (actual.toLowerCase() === indexFile.toLowerCase() && actual !== indexFile) {
161
+ return { expected: indexFile, actual, type: 'file' };
162
+ }
163
+ }
164
+ }
165
+ continue;
166
+ }
167
+
168
+ // Direct file match
169
+ for (const actual of dirFiles) {
170
+ if (actual.toLowerCase() === candidate.toLowerCase()) {
171
+ if (actual !== candidate) {
172
+ return { expected: candidate, actual, type: 'file' };
173
+ }
174
+ // Exact match no case issue
175
+ return null;
176
+ }
177
+ }
178
+ }
179
+
180
+ return null; // File not found at all — not a case issue, just a missing file
181
+ }
182
+
183
+ async function run(projectRoot) {
184
+ const files = collectFiles(projectRoot);
185
+
186
+ if (files.length === 0) {
187
+ return {
188
+ status: 'skip',
189
+ message: `${name} — no JS/TS files found to scan`,
190
+ };
191
+ }
192
+
193
+ const filenameCache = buildFilenameCache(projectRoot);
194
+ const mismatches = [];
195
+
196
+ for (const file of files) {
197
+ let content;
198
+ try {
199
+ content = fs.readFileSync(file, 'utf-8');
200
+ } catch {
201
+ continue;
202
+ }
203
+
204
+ const imports = extractImports(content);
205
+
206
+ for (const imp of imports) {
207
+ const result = resolveImport(imp.importPath, file, filenameCache);
208
+ if (result) {
209
+ const relFile = path.relative(projectRoot, file);
210
+ mismatches.push({
211
+ file: relFile,
212
+ line: imp.line,
213
+ importPath: imp.importPath,
214
+ expected: result.expected,
215
+ actual: result.actual,
216
+ });
217
+ }
218
+ }
219
+ }
220
+
221
+ if (mismatches.length === 0) {
222
+ return {
223
+ status: 'pass',
224
+ message: `${name} — all ${files.length} files checked, no case mismatches found`,
225
+ };
226
+ }
227
+
228
+ return {
229
+ status: 'warn',
230
+ message: `${name} — ${mismatches.length} case mismatch(es) found (will fail on Vercel's Linux filesystem)`,
231
+ fix: 'Rename the import paths to exactly match the filename casing on disk',
232
+ details: mismatches.map((m) => ({
233
+ file: m.file,
234
+ line: m.line,
235
+ message: `Import "${m.importPath}" references "${m.expected}" but file on disk is "${m.actual}"`,
236
+ })),
237
+ };
238
+ }
239
+
240
+ module.exports = { name, run };