@craftpipe/contextpack 1.0.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/.contextpackrc.example.json +167 -0
- package/.env.example +5 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/pull_request_template.md +9 -0
- package/CODE_OF_CONDUCT.md +40 -0
- package/CONTRIBUTING.md +59 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/SECURITY.md +21 -0
- package/index.js +428 -0
- package/lib/analyzer.js +547 -0
- package/lib/bundler.js +477 -0
- package/lib/config.js +269 -0
- package/lib/license.js +180 -0
- package/lib/premium/config-file.js +917 -0
- package/lib/premium/gate.js +13 -0
- package/lib/premium/html-report.js +1094 -0
- package/lib/premium/index.js +57 -0
- package/lib/premium/watch-mode.js +627 -0
- package/lib/scanner.js +480 -0
- package/lib/tokenizer.js +291 -0
- package/lib/validator.js +561 -0
- package/package.json +12 -0
- package/tests/analyzer.test.mjs +128 -0
- package/tests/bundler.test.mjs +126 -0
- package/tests/config.test.mjs +103 -0
- package/tests/gate.test.mjs +118 -0
- package/tests/index.test.mjs +103 -0
- package/tests/license.test.mjs +97 -0
- package/tests/scanner.test.mjs +110 -0
- package/tests/tokenizer.test.mjs +103 -0
- package/tests/validator.test.mjs +111 -0
- package/vitest.config.mjs +13 -0
package/lib/scanner.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default patterns to ignore when scanning
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_IGNORE = [
|
|
10
|
+
'node_modules/**',
|
|
11
|
+
'.git/**',
|
|
12
|
+
'dist/**',
|
|
13
|
+
'build/**',
|
|
14
|
+
'coverage/**',
|
|
15
|
+
'.nyc_output/**',
|
|
16
|
+
'**/*.min.js',
|
|
17
|
+
'**/*.map',
|
|
18
|
+
'.DS_Store',
|
|
19
|
+
'Thumbs.db',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Recursively walk a directory and collect file paths
|
|
24
|
+
* @param {string} dir - Directory to walk
|
|
25
|
+
* @param {string} rootDir - Root directory for relative path calculation
|
|
26
|
+
* @param {string[]} ignorePatterns - Glob-style patterns to ignore
|
|
27
|
+
* @returns {string[]} Array of absolute file paths
|
|
28
|
+
*/
|
|
29
|
+
function walkDir(dir, rootDir, ignorePatterns) {
|
|
30
|
+
const results = [];
|
|
31
|
+
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const fullPath = path.join(dir, entry.name);
|
|
41
|
+
const relPath = path.relative(rootDir, fullPath).replace(/\\/g, '/');
|
|
42
|
+
|
|
43
|
+
if (shouldIgnore(relPath, entry.name, ignorePatterns)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
const nested = walkDir(fullPath, rootDir, ignorePatterns);
|
|
49
|
+
results.push(...nested);
|
|
50
|
+
} else if (entry.isFile()) {
|
|
51
|
+
results.push(fullPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a path matches any ignore pattern
|
|
60
|
+
* @param {string} relPath - Relative path to check
|
|
61
|
+
* @param {string} name - File/directory name
|
|
62
|
+
* @param {string[]} patterns - Patterns to check against
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function shouldIgnore(relPath, name, patterns) {
|
|
66
|
+
if (!patterns || !Array.isArray(patterns)) return false;
|
|
67
|
+
|
|
68
|
+
for (const pattern of patterns) {
|
|
69
|
+
if (matchGlob(pattern, relPath, name)) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Simple glob pattern matcher supporting * and ** wildcards
|
|
78
|
+
* @param {string} pattern - Glob pattern
|
|
79
|
+
* @param {string} relPath - Relative path to test
|
|
80
|
+
* @param {string} name - File/directory name
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function matchGlob(pattern, relPath, name) {
|
|
84
|
+
if (!pattern || typeof pattern !== 'string') return false;
|
|
85
|
+
|
|
86
|
+
// Normalize separators
|
|
87
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
88
|
+
const normalizedPath = relPath.replace(/\\/g, '/');
|
|
89
|
+
|
|
90
|
+
// Convert glob pattern to regex
|
|
91
|
+
const regexStr = normalizedPattern
|
|
92
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape special regex chars except * and ?
|
|
93
|
+
.replace(/\*\*\//g, '(?:.+/)?') // **/ matches zero or more directories
|
|
94
|
+
.replace(/\*\*/g, '.*') // ** matches anything
|
|
95
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
96
|
+
.replace(/\?/g, '[^/]'); // ? matches single char except /
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
100
|
+
if (regex.test(normalizedPath)) return true;
|
|
101
|
+
|
|
102
|
+
// Also test just the filename for simple patterns without /
|
|
103
|
+
if (!normalizedPattern.includes('/') && regex.test(name)) return true;
|
|
104
|
+
|
|
105
|
+
// Test if path starts with pattern (for directory patterns like node_modules/**)
|
|
106
|
+
const prefixRegex = new RegExp(`(^|/)${regexStr}(/|$)`);
|
|
107
|
+
if (prefixRegex.test(normalizedPath)) return true;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// If regex fails, fall back to simple string comparison
|
|
110
|
+
return normalizedPath === normalizedPattern || name === normalizedPattern;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get file metadata for a given path
|
|
118
|
+
* @param {string} filePath - Absolute path to file
|
|
119
|
+
* @param {string} rootDir - Root directory for relative path
|
|
120
|
+
* @returns {object|null} File metadata object or null on error
|
|
121
|
+
*/
|
|
122
|
+
function getFileMetadata(filePath, rootDir) {
|
|
123
|
+
try {
|
|
124
|
+
const stats = fs.statSync(filePath);
|
|
125
|
+
const relPath = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
126
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
127
|
+
const name = path.basename(filePath);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
path: relPath,
|
|
131
|
+
absolutePath: filePath,
|
|
132
|
+
name,
|
|
133
|
+
ext,
|
|
134
|
+
size: stats.size,
|
|
135
|
+
sizeKB: Math.round((stats.size / 1024) * 100) / 100,
|
|
136
|
+
modified: stats.mtime.toISOString(),
|
|
137
|
+
created: stats.birthtime.toISOString(),
|
|
138
|
+
type: getFileType(ext),
|
|
139
|
+
directory: path.dirname(relPath).replace(/\\/g, '/'),
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Determine file type category from extension
|
|
148
|
+
* @param {string} ext - File extension including dot
|
|
149
|
+
* @returns {string} File type category
|
|
150
|
+
*/
|
|
151
|
+
function getFileType(ext) {
|
|
152
|
+
const typeMap = {
|
|
153
|
+
'.js': 'javascript',
|
|
154
|
+
'.mjs': 'javascript',
|
|
155
|
+
'.cjs': 'javascript',
|
|
156
|
+
'.jsx': 'javascript',
|
|
157
|
+
'.ts': 'typescript',
|
|
158
|
+
'.tsx': 'typescript',
|
|
159
|
+
'.json': 'json',
|
|
160
|
+
'.md': 'markdown',
|
|
161
|
+
'.mdx': 'markdown',
|
|
162
|
+
'.html': 'html',
|
|
163
|
+
'.htm': 'html',
|
|
164
|
+
'.css': 'css',
|
|
165
|
+
'.scss': 'css',
|
|
166
|
+
'.sass': 'css',
|
|
167
|
+
'.less': 'css',
|
|
168
|
+
'.py': 'python',
|
|
169
|
+
'.rb': 'ruby',
|
|
170
|
+
'.go': 'go',
|
|
171
|
+
'.rs': 'rust',
|
|
172
|
+
'.java': 'java',
|
|
173
|
+
'.c': 'c',
|
|
174
|
+
'.cpp': 'cpp',
|
|
175
|
+
'.h': 'c',
|
|
176
|
+
'.hpp': 'cpp',
|
|
177
|
+
'.sh': 'shell',
|
|
178
|
+
'.bash': 'shell',
|
|
179
|
+
'.zsh': 'shell',
|
|
180
|
+
'.yaml': 'yaml',
|
|
181
|
+
'.yml': 'yaml',
|
|
182
|
+
'.toml': 'toml',
|
|
183
|
+
'.xml': 'xml',
|
|
184
|
+
'.svg': 'svg',
|
|
185
|
+
'.txt': 'text',
|
|
186
|
+
'.env': 'env',
|
|
187
|
+
'.gitignore': 'config',
|
|
188
|
+
'.eslintrc': 'config',
|
|
189
|
+
'.prettierrc': 'config',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return typeMap[ext] || 'other';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Scans a project directory and returns a raw file inventory with paths and metadata.
|
|
197
|
+
* @param {string} projectDir - Root directory to scan
|
|
198
|
+
* @param {object} options - Scan options
|
|
199
|
+
* @param {string[]} [options.ignore] - Additional patterns to ignore
|
|
200
|
+
* @param {string[]} [options.extensions] - Only include files with these extensions
|
|
201
|
+
* @param {boolean} [options.includeHidden] - Whether to include hidden files (default: false)
|
|
202
|
+
* @returns {object} File inventory with files array and summary stats
|
|
203
|
+
*/
|
|
204
|
+
function scanProject(projectDir, options) {
|
|
205
|
+
const opts = options || {};
|
|
206
|
+
const rootDir = projectDir || process.cwd();
|
|
207
|
+
|
|
208
|
+
// Validate directory exists
|
|
209
|
+
try {
|
|
210
|
+
const stat = fs.statSync(rootDir);
|
|
211
|
+
if (!stat.isDirectory()) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: `Path is not a directory: ${rootDir}`,
|
|
215
|
+
files: [],
|
|
216
|
+
summary: { total: 0, totalSize: 0, byType: {}, byExtension: {} },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
error: `Cannot access directory: ${rootDir} — ${err.message}`,
|
|
223
|
+
files: [],
|
|
224
|
+
summary: { total: 0, totalSize: 0, byType: {}, byExtension: {} },
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const ignorePatterns = [...DEFAULT_IGNORE, ...(opts.ignore || [])];
|
|
229
|
+
|
|
230
|
+
// Optionally exclude hidden files/dirs
|
|
231
|
+
if (!opts.includeHidden) {
|
|
232
|
+
ignorePatterns.push('.*', '.*/**');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Walk the directory
|
|
236
|
+
const absolutePaths = walkDir(rootDir, rootDir, ignorePatterns);
|
|
237
|
+
|
|
238
|
+
// Gather metadata for each file
|
|
239
|
+
let files = [];
|
|
240
|
+
for (const absPath of absolutePaths) {
|
|
241
|
+
const meta = getFileMetadata(absPath, rootDir);
|
|
242
|
+
if (meta) {
|
|
243
|
+
files.push(meta);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Filter by extension if specified
|
|
248
|
+
if (opts.extensions && Array.isArray(opts.extensions) && opts.extensions.length > 0) {
|
|
249
|
+
const exts = opts.extensions.map(e => (e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
250
|
+
files = files.filter(f => exts.includes(f.ext));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Build summary stats
|
|
254
|
+
const summary = buildSummary(files);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
rootDir,
|
|
259
|
+
files,
|
|
260
|
+
summary,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build summary statistics from file list
|
|
266
|
+
* @param {object[]} files - Array of file metadata objects
|
|
267
|
+
* @returns {object} Summary statistics
|
|
268
|
+
*/
|
|
269
|
+
function buildSummary(files) {
|
|
270
|
+
const byType = {};
|
|
271
|
+
const byExtension = {};
|
|
272
|
+
let totalSize = 0;
|
|
273
|
+
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
totalSize += file.size || 0;
|
|
276
|
+
|
|
277
|
+
byType[file.type] = (byType[file.type] || 0) + 1;
|
|
278
|
+
|
|
279
|
+
const ext = file.ext || '(none)';
|
|
280
|
+
byExtension[ext] = (byExtension[ext] || 0) + 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
total: files.length,
|
|
285
|
+
totalSize,
|
|
286
|
+
totalSizeKB: Math.round((totalSize / 1024) * 100) / 100,
|
|
287
|
+
byType,
|
|
288
|
+
byExtension,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Builds a hierarchical file tree structure from a flat list of file metadata.
|
|
294
|
+
* @param {object[]} files - Array of file metadata objects (from scanProject)
|
|
295
|
+
* @param {object} options - Options
|
|
296
|
+
* @param {boolean} [options.includeMetadata] - Include full metadata on leaf nodes (default: true)
|
|
297
|
+
* @returns {object} Nested tree structure representing the file hierarchy
|
|
298
|
+
*/
|
|
299
|
+
function buildFileTree(files, options) {
|
|
300
|
+
const opts = options || {};
|
|
301
|
+
const includeMetadata = opts.includeMetadata !== false;
|
|
302
|
+
|
|
303
|
+
if (!files || !Array.isArray(files)) {
|
|
304
|
+
return { name: '.', type: 'directory', children: {} };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const tree = {
|
|
308
|
+
name: '.',
|
|
309
|
+
type: 'directory',
|
|
310
|
+
children: {},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
if (!file || !file.path) continue;
|
|
315
|
+
|
|
316
|
+
const parts = file.path.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
317
|
+
let node = tree;
|
|
318
|
+
|
|
319
|
+
// Traverse/create directory nodes
|
|
320
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
321
|
+
const part = parts[i];
|
|
322
|
+
if (!node.children[part]) {
|
|
323
|
+
node.children[part] = {
|
|
324
|
+
name: part,
|
|
325
|
+
type: 'directory',
|
|
326
|
+
path: parts.slice(0, i + 1).join('/'),
|
|
327
|
+
children: {},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
node = node.children[part];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Add the file leaf node
|
|
334
|
+
const fileName = parts[parts.length - 1];
|
|
335
|
+
const leafNode = {
|
|
336
|
+
name: fileName,
|
|
337
|
+
type: 'file',
|
|
338
|
+
path: file.path,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (includeMetadata) {
|
|
342
|
+
leafNode.size = file.size;
|
|
343
|
+
leafNode.sizeKB = file.sizeKB;
|
|
344
|
+
leafNode.ext = file.ext;
|
|
345
|
+
leafNode.fileType = file.type;
|
|
346
|
+
leafNode.modified = file.modified;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
node.children[fileName] = leafNode;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return tree;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Filters a file inventory by type, extension, directory, or custom predicate.
|
|
357
|
+
* @param {object[]|object} filesOrInventory - Array of file metadata objects, or full scanProject result
|
|
358
|
+
* @param {object} filterOptions - Filter criteria
|
|
359
|
+
* @param {string|string[]} [filterOptions.types] - File type(s) to include (e.g. 'javascript', 'json')
|
|
360
|
+
* @param {string|string[]} [filterOptions.extensions] - Extension(s) to include (e.g. '.js', '.ts')
|
|
361
|
+
* @param {string|string[]} [filterOptions.directories] - Only include files under these directories
|
|
362
|
+
* @param {string|string[]} [filterOptions.excludeDirectories] - Exclude files under these directories
|
|
363
|
+
* @param {string|string[]} [filterOptions.excludeTypes] - File type(s) to exclude
|
|
364
|
+
* @param {string|string[]} [filterOptions.excludeExtensions] - Extension(s) to exclude
|
|
365
|
+
* @param {number} [filterOptions.maxSize] - Maximum file size in bytes
|
|
366
|
+
* @param {number} [filterOptions.minSize] - Minimum file size in bytes
|
|
367
|
+
* @param {Function} [filterOptions.predicate] - Custom filter function (file) => boolean
|
|
368
|
+
* @returns {object[]} Filtered array of file metadata objects
|
|
369
|
+
*/
|
|
370
|
+
function filterFiles(filesOrInventory, filterOptions) {
|
|
371
|
+
const opts = filterOptions || {};
|
|
372
|
+
|
|
373
|
+
// Accept either a raw array or a scanProject result object
|
|
374
|
+
let files;
|
|
375
|
+
if (Array.isArray(filesOrInventory)) {
|
|
376
|
+
files = filesOrInventory;
|
|
377
|
+
} else if (filesOrInventory && Array.isArray(filesOrInventory.files)) {
|
|
378
|
+
files = filesOrInventory.files;
|
|
379
|
+
} else {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Normalize filter values to arrays
|
|
384
|
+
const types = normalizeToArray(opts.types);
|
|
385
|
+
const extensions = normalizeToArray(opts.extensions).map(e =>
|
|
386
|
+
e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
387
|
+
);
|
|
388
|
+
const directories = normalizeToArray(opts.directories).map(d => d.replace(/\\/g, '/'));
|
|
389
|
+
const excludeDirectories = normalizeToArray(opts.excludeDirectories).map(d => d.replace(/\\/g, '/'));
|
|
390
|
+
const excludeTypes = normalizeToArray(opts.excludeTypes);
|
|
391
|
+
const excludeExtensions = normalizeToArray(opts.excludeExtensions).map(e =>
|
|
392
|
+
e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
return files.filter(file => {
|
|
396
|
+
if (!file) return false;
|
|
397
|
+
|
|
398
|
+
// Include by type
|
|
399
|
+
if (types.length > 0 && !types.includes(file.type)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Include by extension
|
|
404
|
+
if (extensions.length > 0 && !extensions.includes(file.ext)) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Include by directory (file must be under one of these dirs)
|
|
409
|
+
if (directories.length > 0) {
|
|
410
|
+
const fileDir = (file.directory || '').replace(/\\/g, '/');
|
|
411
|
+
const filePath = (file.path || '').replace(/\\/g, '/');
|
|
412
|
+
const inDir = directories.some(dir => {
|
|
413
|
+
const normalDir = dir.endsWith('/') ? dir : `${dir}/`;
|
|
414
|
+
return fileDir === dir ||
|
|
415
|
+
fileDir.startsWith(normalDir) ||
|
|
416
|
+
filePath.startsWith(normalDir) ||
|
|
417
|
+
filePath === dir;
|
|
418
|
+
});
|
|
419
|
+
if (!inDir) return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Exclude by directory
|
|
423
|
+
if (excludeDirectories.length > 0) {
|
|
424
|
+
const fileDir = (file.directory || '').replace(/\\/g, '/');
|
|
425
|
+
const filePath = (file.path || '').replace(/\\/g, '/');
|
|
426
|
+
const inExcluded = excludeDirectories.some(dir => {
|
|
427
|
+
const normalDir = dir.endsWith('/') ? dir : `${dir}/`;
|
|
428
|
+
return fileDir === dir ||
|
|
429
|
+
fileDir.startsWith(normalDir) ||
|
|
430
|
+
filePath.startsWith(normalDir);
|
|
431
|
+
});
|
|
432
|
+
if (inExcluded) return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Exclude by type
|
|
436
|
+
if (excludeTypes.length > 0 && excludeTypes.includes(file.type)) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Exclude by extension
|
|
441
|
+
if (excludeExtensions.length > 0 && excludeExtensions.includes(file.ext)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Filter by max size
|
|
446
|
+
if (opts.maxSize !== undefined && opts.maxSize !== null) {
|
|
447
|
+
if (file.size > opts.maxSize) return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Filter by min size
|
|
451
|
+
if (opts.minSize !== undefined && opts.minSize !== null) {
|
|
452
|
+
if (file.size < opts.minSize) return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Custom predicate
|
|
456
|
+
if (typeof opts.predicate === 'function') {
|
|
457
|
+
try {
|
|
458
|
+
if (!opts.predicate(file)) return false;
|
|
459
|
+
} catch (e) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return true;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Normalize a value to an array, handling null/undefined/string/array
|
|
470
|
+
* @param {*} val
|
|
471
|
+
* @returns {string[]}
|
|
472
|
+
*/
|
|
473
|
+
function normalizeToArray(val) {
|
|
474
|
+
if (!val) return [];
|
|
475
|
+
if (Array.isArray(val)) return val.filter(Boolean);
|
|
476
|
+
if (typeof val === 'string') return [val];
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = { scanProject, buildFileTree, filterFiles };
|