predeploy-check 1.0.0 → 1.1.0
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/LICENSE +21 -0
- package/README.md +83 -74
- package/bin/cli.js +50 -48
- package/checks/01-python-render.js +207 -123
- package/checks/03-case-sensitivity.js +240 -228
- package/index.js +137 -130
- package/package.json +37 -37
|
@@ -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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
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 };
|