@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/validator.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* lib/validator.js
|
|
7
|
+
* Validates bundle integrity; checks for missing files, circular dependencies,
|
|
8
|
+
* and symbol conflicts; returns a structured validation report with categorized
|
|
9
|
+
* warnings and errors.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Severity levels for validation issues
|
|
14
|
+
*/
|
|
15
|
+
const SEVERITY = {
|
|
16
|
+
ERROR: 'error',
|
|
17
|
+
WARNING: 'warning',
|
|
18
|
+
INFO: 'info',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a validation issue object
|
|
23
|
+
* @param {string} severity - One of 'error', 'warning', 'info'
|
|
24
|
+
* @param {string} code - Short machine-readable code for the issue
|
|
25
|
+
* @param {string} message - Human-readable description
|
|
26
|
+
* @param {object} [meta] - Optional additional metadata
|
|
27
|
+
* @returns {object} Issue object
|
|
28
|
+
*/
|
|
29
|
+
function createIssue(severity, code, message, meta) {
|
|
30
|
+
return {
|
|
31
|
+
severity,
|
|
32
|
+
code,
|
|
33
|
+
message,
|
|
34
|
+
meta: meta || {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create an empty validation report
|
|
40
|
+
* @returns {object} Empty report structure
|
|
41
|
+
*/
|
|
42
|
+
function createReport() {
|
|
43
|
+
return {
|
|
44
|
+
valid: true,
|
|
45
|
+
errors: [],
|
|
46
|
+
warnings: [],
|
|
47
|
+
infos: [],
|
|
48
|
+
summary: {
|
|
49
|
+
errorCount: 0,
|
|
50
|
+
warningCount: 0,
|
|
51
|
+
infoCount: 0,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add an issue to the report and update summary counts
|
|
58
|
+
* @param {object} report - The validation report to mutate
|
|
59
|
+
* @param {object} issue - The issue to add
|
|
60
|
+
*/
|
|
61
|
+
function addIssue(report, issue) {
|
|
62
|
+
if (!report || !issue) return;
|
|
63
|
+
|
|
64
|
+
switch (issue.severity) {
|
|
65
|
+
case SEVERITY.ERROR:
|
|
66
|
+
report.errors.push(issue);
|
|
67
|
+
report.summary.errorCount += 1;
|
|
68
|
+
report.valid = false;
|
|
69
|
+
break;
|
|
70
|
+
case SEVERITY.WARNING:
|
|
71
|
+
report.warnings.push(issue);
|
|
72
|
+
report.summary.warningCount += 1;
|
|
73
|
+
break;
|
|
74
|
+
case SEVERITY.INFO:
|
|
75
|
+
report.infos.push(issue);
|
|
76
|
+
report.summary.infoCount += 1;
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
report.warnings.push(issue);
|
|
80
|
+
report.summary.warningCount += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate that required top-level fields exist in the bundle
|
|
86
|
+
* @param {object} bundle - The bundle to validate
|
|
87
|
+
* @param {object} report - The report to add issues to
|
|
88
|
+
*/
|
|
89
|
+
function validateBundleStructure(bundle, report) {
|
|
90
|
+
const requiredFields = ['fileSummaries', 'symbolIndex', 'dependencyMap', 'metadata'];
|
|
91
|
+
|
|
92
|
+
for (const field of requiredFields) {
|
|
93
|
+
if (!(field in bundle)) {
|
|
94
|
+
addIssue(report, createIssue(
|
|
95
|
+
SEVERITY.ERROR,
|
|
96
|
+
'MISSING_FIELD',
|
|
97
|
+
`Bundle is missing required field: "${field}"`,
|
|
98
|
+
{ field }
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ('metadata' in bundle) {
|
|
104
|
+
const metadata = bundle.metadata || {};
|
|
105
|
+
if (!metadata.projectDir) {
|
|
106
|
+
addIssue(report, createIssue(
|
|
107
|
+
SEVERITY.WARNING,
|
|
108
|
+
'MISSING_METADATA_PROJECT_DIR',
|
|
109
|
+
'Bundle metadata is missing "projectDir"',
|
|
110
|
+
{}
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
if (!metadata.createdAt) {
|
|
114
|
+
addIssue(report, createIssue(
|
|
115
|
+
SEVERITY.INFO,
|
|
116
|
+
'MISSING_METADATA_CREATED_AT',
|
|
117
|
+
'Bundle metadata is missing "createdAt" timestamp',
|
|
118
|
+
{}
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
if (metadata.error) {
|
|
122
|
+
addIssue(report, createIssue(
|
|
123
|
+
SEVERITY.ERROR,
|
|
124
|
+
'BUNDLE_CREATION_ERROR',
|
|
125
|
+
`Bundle was created with an error: ${metadata.error}`,
|
|
126
|
+
{ error: metadata.error }
|
|
127
|
+
));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate fileSummaries array for missing or malformed entries
|
|
134
|
+
* @param {object} bundle - The bundle to validate
|
|
135
|
+
* @param {object} report - The report to add issues to
|
|
136
|
+
*/
|
|
137
|
+
function validateFileSummaries(bundle, report) {
|
|
138
|
+
const fileSummaries = bundle.fileSummaries;
|
|
139
|
+
|
|
140
|
+
if (!Array.isArray(fileSummaries)) {
|
|
141
|
+
addIssue(report, createIssue(
|
|
142
|
+
SEVERITY.ERROR,
|
|
143
|
+
'INVALID_FILE_SUMMARIES',
|
|
144
|
+
'"fileSummaries" must be an array',
|
|
145
|
+
{}
|
|
146
|
+
));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (fileSummaries.length === 0) {
|
|
151
|
+
addIssue(report, createIssue(
|
|
152
|
+
SEVERITY.WARNING,
|
|
153
|
+
'EMPTY_FILE_SUMMARIES',
|
|
154
|
+
'Bundle contains no file summaries',
|
|
155
|
+
{}
|
|
156
|
+
));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const seenPaths = new Set();
|
|
161
|
+
|
|
162
|
+
fileSummaries.forEach(function (entry, index) {
|
|
163
|
+
if (!entry || typeof entry !== 'object') {
|
|
164
|
+
addIssue(report, createIssue(
|
|
165
|
+
SEVERITY.ERROR,
|
|
166
|
+
'INVALID_FILE_SUMMARY_ENTRY',
|
|
167
|
+
`fileSummaries[${index}] is not a valid object`,
|
|
168
|
+
{ index }
|
|
169
|
+
));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!entry.path || typeof entry.path !== 'string') {
|
|
174
|
+
addIssue(report, createIssue(
|
|
175
|
+
SEVERITY.ERROR,
|
|
176
|
+
'MISSING_FILE_PATH',
|
|
177
|
+
`fileSummaries[${index}] is missing a valid "path" field`,
|
|
178
|
+
{ index }
|
|
179
|
+
));
|
|
180
|
+
} else {
|
|
181
|
+
if (seenPaths.has(entry.path)) {
|
|
182
|
+
addIssue(report, createIssue(
|
|
183
|
+
SEVERITY.WARNING,
|
|
184
|
+
'DUPLICATE_FILE_PATH',
|
|
185
|
+
`Duplicate file path detected in fileSummaries: "${entry.path}"`,
|
|
186
|
+
{ path: entry.path, index }
|
|
187
|
+
));
|
|
188
|
+
} else {
|
|
189
|
+
seenPaths.add(entry.path);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (entry.size !== undefined && (typeof entry.size !== 'number' || entry.size < 0)) {
|
|
194
|
+
addIssue(report, createIssue(
|
|
195
|
+
SEVERITY.WARNING,
|
|
196
|
+
'INVALID_FILE_SIZE',
|
|
197
|
+
`fileSummaries[${index}] has an invalid "size" value: ${entry.size}`,
|
|
198
|
+
{ index, path: entry.path || null, size: entry.size }
|
|
199
|
+
));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate the symbolIndex for structural integrity
|
|
206
|
+
* @param {object} bundle - The bundle to validate
|
|
207
|
+
* @param {object} report - The report to add issues to
|
|
208
|
+
*/
|
|
209
|
+
function validateSymbolIndex(bundle, report) {
|
|
210
|
+
const symbolIndex = bundle.symbolIndex;
|
|
211
|
+
|
|
212
|
+
if (symbolIndex === null || symbolIndex === undefined) {
|
|
213
|
+
addIssue(report, createIssue(
|
|
214
|
+
SEVERITY.WARNING,
|
|
215
|
+
'NULL_SYMBOL_INDEX',
|
|
216
|
+
'"symbolIndex" is null or undefined',
|
|
217
|
+
{}
|
|
218
|
+
));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof symbolIndex !== 'object' || Array.isArray(symbolIndex)) {
|
|
223
|
+
addIssue(report, createIssue(
|
|
224
|
+
SEVERITY.ERROR,
|
|
225
|
+
'INVALID_SYMBOL_INDEX',
|
|
226
|
+
'"symbolIndex" must be a plain object',
|
|
227
|
+
{}
|
|
228
|
+
));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const filePaths = Object.keys(symbolIndex);
|
|
233
|
+
|
|
234
|
+
if (filePaths.length === 0) {
|
|
235
|
+
addIssue(report, createIssue(
|
|
236
|
+
SEVERITY.INFO,
|
|
237
|
+
'EMPTY_SYMBOL_INDEX',
|
|
238
|
+
'"symbolIndex" contains no entries',
|
|
239
|
+
{}
|
|
240
|
+
));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const filePath of filePaths) {
|
|
245
|
+
const entry = symbolIndex[filePath];
|
|
246
|
+
if (!entry || typeof entry !== 'object') {
|
|
247
|
+
addIssue(report, createIssue(
|
|
248
|
+
SEVERITY.WARNING,
|
|
249
|
+
'INVALID_SYMBOL_ENTRY',
|
|
250
|
+
`symbolIndex entry for "${filePath}" is not a valid object`,
|
|
251
|
+
{ filePath }
|
|
252
|
+
));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (entry.symbols !== undefined && !Array.isArray(entry.symbols)) {
|
|
257
|
+
addIssue(report, createIssue(
|
|
258
|
+
SEVERITY.WARNING,
|
|
259
|
+
'INVALID_SYMBOLS_FIELD',
|
|
260
|
+
`symbolIndex["${filePath}"].symbols must be an array`,
|
|
261
|
+
{ filePath }
|
|
262
|
+
));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Validate the dependencyMap for structural integrity and missing referenced files
|
|
269
|
+
* @param {object} bundle - The bundle to validate
|
|
270
|
+
* @param {object} report - The report to add issues to
|
|
271
|
+
* @param {Set} knownPaths - Set of known file paths from fileSummaries
|
|
272
|
+
*/
|
|
273
|
+
function validateDependencyMap(bundle, report, knownPaths) {
|
|
274
|
+
const dependencyMap = bundle.dependencyMap;
|
|
275
|
+
|
|
276
|
+
if (dependencyMap === null || dependencyMap === undefined) {
|
|
277
|
+
addIssue(report, createIssue(
|
|
278
|
+
SEVERITY.WARNING,
|
|
279
|
+
'NULL_DEPENDENCY_MAP',
|
|
280
|
+
'"dependencyMap" is null or undefined',
|
|
281
|
+
{}
|
|
282
|
+
));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (typeof dependencyMap !== 'object' || Array.isArray(dependencyMap)) {
|
|
287
|
+
addIssue(report, createIssue(
|
|
288
|
+
SEVERITY.ERROR,
|
|
289
|
+
'INVALID_DEPENDENCY_MAP',
|
|
290
|
+
'"dependencyMap" must be a plain object',
|
|
291
|
+
{}
|
|
292
|
+
));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const depKeys = Object.keys(dependencyMap);
|
|
297
|
+
|
|
298
|
+
for (const filePath of depKeys) {
|
|
299
|
+
const deps = dependencyMap[filePath];
|
|
300
|
+
|
|
301
|
+
if (!Array.isArray(deps)) {
|
|
302
|
+
addIssue(report, createIssue(
|
|
303
|
+
SEVERITY.WARNING,
|
|
304
|
+
'INVALID_DEPENDENCY_ENTRY',
|
|
305
|
+
`dependencyMap["${filePath}"] must be an array of dependency paths`,
|
|
306
|
+
{ filePath }
|
|
307
|
+
));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const dep of deps) {
|
|
312
|
+
if (typeof dep !== 'string') {
|
|
313
|
+
addIssue(report, createIssue(
|
|
314
|
+
SEVERITY.WARNING,
|
|
315
|
+
'INVALID_DEPENDENCY_VALUE',
|
|
316
|
+
`dependencyMap["${filePath}"] contains a non-string dependency value`,
|
|
317
|
+
{ filePath, dep }
|
|
318
|
+
));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Only flag missing files for relative/local dependencies (not node_modules)
|
|
323
|
+
const isLocalDep = dep.startsWith('.') || dep.startsWith('/');
|
|
324
|
+
if (isLocalDep && knownPaths.size > 0 && !knownPaths.has(dep)) {
|
|
325
|
+
// Normalize and try again
|
|
326
|
+
const normalized = dep.replace(/\\/g, '/');
|
|
327
|
+
if (!knownPaths.has(normalized)) {
|
|
328
|
+
addIssue(report, createIssue(
|
|
329
|
+
SEVERITY.WARNING,
|
|
330
|
+
'MISSING_DEPENDENCY_FILE',
|
|
331
|
+
`File "${filePath}" depends on "${dep}" which is not present in the bundle`,
|
|
332
|
+
{ filePath, dependency: dep }
|
|
333
|
+
));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Detect circular dependencies in a dependency graph using DFS
|
|
342
|
+
* @param {object} dependencyMap - Map of file -> array of dependencies
|
|
343
|
+
* @returns {Array<string[]>} Array of cycles, each cycle is an array of file paths forming the loop
|
|
344
|
+
*/
|
|
345
|
+
function checkCircularDependencies(dependencyMap) {
|
|
346
|
+
if (!dependencyMap || typeof dependencyMap !== 'object' || Array.isArray(dependencyMap)) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const cycles = [];
|
|
351
|
+
const visited = new Set();
|
|
352
|
+
const inStack = new Set();
|
|
353
|
+
const stackPath = [];
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* DFS visit function
|
|
357
|
+
* @param {string} node - Current node
|
|
358
|
+
*/
|
|
359
|
+
function dfs(node) {
|
|
360
|
+
if (inStack.has(node)) {
|
|
361
|
+
// Found a cycle — extract the cycle portion from stackPath
|
|
362
|
+
const cycleStart = stackPath.indexOf(node);
|
|
363
|
+
if (cycleStart !== -1) {
|
|
364
|
+
const cycle = stackPath.slice(cycleStart).concat(node);
|
|
365
|
+
// Deduplicate cycles by canonical form
|
|
366
|
+
const cycleKey = cycle.slice().sort().join('|');
|
|
367
|
+
const alreadyFound = cycles.some(function (existing) {
|
|
368
|
+
return existing.slice().sort().join('|') === cycleKey;
|
|
369
|
+
});
|
|
370
|
+
if (!alreadyFound) {
|
|
371
|
+
cycles.push(cycle);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (visited.has(node)) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
visited.add(node);
|
|
382
|
+
inStack.add(node);
|
|
383
|
+
stackPath.push(node);
|
|
384
|
+
|
|
385
|
+
const deps = dependencyMap[node];
|
|
386
|
+
if (Array.isArray(deps)) {
|
|
387
|
+
for (const dep of deps) {
|
|
388
|
+
if (typeof dep === 'string') {
|
|
389
|
+
dfs(dep);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
stackPath.pop();
|
|
395
|
+
inStack.delete(node);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const allNodes = Object.keys(dependencyMap);
|
|
399
|
+
for (const node of allNodes) {
|
|
400
|
+
if (!visited.has(node)) {
|
|
401
|
+
dfs(node);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return cycles;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Detect symbol conflicts across files in the symbol index.
|
|
410
|
+
* A conflict occurs when the same symbol name is exported from multiple files.
|
|
411
|
+
* @param {object} symbolIndex - Map of filePath -> { symbols: string[] }
|
|
412
|
+
* @returns {Array<object>} Array of conflict objects { symbol, files }
|
|
413
|
+
*/
|
|
414
|
+
function checkSymbolConflicts(symbolIndex) {
|
|
415
|
+
if (!symbolIndex || typeof symbolIndex !== 'object' || Array.isArray(symbolIndex)) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Build a map of symbol name -> array of files that export it
|
|
420
|
+
const symbolToFiles = {};
|
|
421
|
+
|
|
422
|
+
const filePaths = Object.keys(symbolIndex);
|
|
423
|
+
|
|
424
|
+
for (const filePath of filePaths) {
|
|
425
|
+
const entry = symbolIndex[filePath];
|
|
426
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
427
|
+
|
|
428
|
+
const symbols = entry.symbols;
|
|
429
|
+
if (!Array.isArray(symbols)) continue;
|
|
430
|
+
|
|
431
|
+
for (const sym of symbols) {
|
|
432
|
+
let symbolName = null;
|
|
433
|
+
|
|
434
|
+
if (typeof sym === 'string') {
|
|
435
|
+
symbolName = sym;
|
|
436
|
+
} else if (sym && typeof sym === 'object' && typeof sym.name === 'string') {
|
|
437
|
+
symbolName = sym.name;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!symbolName) continue;
|
|
441
|
+
|
|
442
|
+
if (!symbolToFiles[symbolName]) {
|
|
443
|
+
symbolToFiles[symbolName] = [];
|
|
444
|
+
}
|
|
445
|
+
if (!symbolToFiles[symbolName].includes(filePath)) {
|
|
446
|
+
symbolToFiles[symbolName].push(filePath);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Collect conflicts (symbol exported from more than one file)
|
|
452
|
+
const conflicts = [];
|
|
453
|
+
|
|
454
|
+
for (const symbolName of Object.keys(symbolToFiles)) {
|
|
455
|
+
const files = symbolToFiles[symbolName];
|
|
456
|
+
if (files.length > 1) {
|
|
457
|
+
conflicts.push({
|
|
458
|
+
symbol: symbolName,
|
|
459
|
+
files: files.slice(),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return conflicts;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Validate a context bundle for integrity issues including missing files,
|
|
469
|
+
* circular dependencies, and symbol conflicts.
|
|
470
|
+
*
|
|
471
|
+
* @param {object} bundle - The bundle object produced by createBundle()
|
|
472
|
+
* @param {object} [options] - Optional validation options
|
|
473
|
+
* @param {boolean} [options.checkCircular=true] - Whether to check for circular dependencies
|
|
474
|
+
* @param {boolean} [options.checkConflicts=true] - Whether to check for symbol conflicts
|
|
475
|
+
* @param {boolean} [options.checkMissingFiles=true] - Whether to check for missing dependency files
|
|
476
|
+
* @returns {object} Validation report with { valid, errors, warnings, infos, summary }
|
|
477
|
+
*/
|
|
478
|
+
function validateBundle(bundle, options) {
|
|
479
|
+
const opts = options || {};
|
|
480
|
+
const {
|
|
481
|
+
checkCircular = true,
|
|
482
|
+
checkConflicts = true,
|
|
483
|
+
checkMissingFiles = true,
|
|
484
|
+
} = opts;
|
|
485
|
+
|
|
486
|
+
const report = createReport();
|
|
487
|
+
|
|
488
|
+
// Handle null/undefined bundle
|
|
489
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
490
|
+
addIssue(report, createIssue(
|
|
491
|
+
SEVERITY.ERROR,
|
|
492
|
+
'INVALID_BUNDLE',
|
|
493
|
+
'Bundle must be a non-null object',
|
|
494
|
+
{}
|
|
495
|
+
));
|
|
496
|
+
return report;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 1. Validate top-level structure
|
|
500
|
+
validateBundleStructure(bundle, report);
|
|
501
|
+
|
|
502
|
+
// 2. Validate fileSummaries
|
|
503
|
+
if ('fileSummaries' in bundle) {
|
|
504
|
+
validateFileSummaries(bundle, report);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 3. Validate symbolIndex
|
|
508
|
+
if ('symbolIndex' in bundle) {
|
|
509
|
+
validateSymbolIndex(bundle, report);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Build a set of known paths from fileSummaries for cross-reference checks
|
|
513
|
+
const knownPaths = new Set();
|
|
514
|
+
if (Array.isArray(bundle.fileSummaries)) {
|
|
515
|
+
for (const entry of bundle.fileSummaries) {
|
|
516
|
+
if (entry && typeof entry.path === 'string') {
|
|
517
|
+
knownPaths.add(entry.path);
|
|
518
|
+
knownPaths.add(entry.path.replace(/\\/g, '/'));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 4. Validate dependencyMap
|
|
524
|
+
if ('dependencyMap' in bundle) {
|
|
525
|
+
validateDependencyMap(bundle, report, checkMissingFiles ? knownPaths : new Set());
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 5. Check for circular dependencies
|
|
529
|
+
if (checkCircular && bundle.dependencyMap && typeof bundle.dependencyMap === 'object') {
|
|
530
|
+
const cycles = checkCircularDependencies(bundle.dependencyMap);
|
|
531
|
+
for (const cycle of cycles) {
|
|
532
|
+
addIssue(report, createIssue(
|
|
533
|
+
SEVERITY.ERROR,
|
|
534
|
+
'CIRCULAR_DEPENDENCY',
|
|
535
|
+
`Circular dependency detected: ${cycle.join(' -> ')}`,
|
|
536
|
+
{ cycle }
|
|
537
|
+
));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 6. Check for symbol conflicts
|
|
542
|
+
if (checkConflicts && bundle.symbolIndex && typeof bundle.symbolIndex === 'object') {
|
|
543
|
+
const conflicts = checkSymbolConflicts(bundle.symbolIndex);
|
|
544
|
+
for (const conflict of conflicts) {
|
|
545
|
+
addIssue(report, createIssue(
|
|
546
|
+
SEVERITY.WARNING,
|
|
547
|
+
'SYMBOL_CONFLICT',
|
|
548
|
+
`Symbol "${conflict.symbol}" is exported from multiple files: ${conflict.files.join(', ')}`,
|
|
549
|
+
{ symbol: conflict.symbol, files: conflict.files }
|
|
550
|
+
));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return report;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
module.exports = {
|
|
558
|
+
validateBundle,
|
|
559
|
+
checkCircularDependencies,
|
|
560
|
+
checkSymbolConflicts,
|
|
561
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import analyzerModule from '../lib/analyzer.js';
|
|
3
|
+
|
|
4
|
+
const { analyzeFile, analyzeProject, extractSymbols, buildDependencyGraph } = analyzerModule;
|
|
5
|
+
|
|
6
|
+
vi.mock('fs', async (importOriginal) => {
|
|
7
|
+
const actual = await importOriginal();
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
readFileSync: vi.fn(() => 'const foo = () => {};\nfunction bar() {}\nmodule.exports = { foo, bar };'),
|
|
11
|
+
existsSync: vi.fn(() => true),
|
|
12
|
+
statSync: vi.fn(() => ({ size: 100, isDirectory: () => false, isFile: () => true })),
|
|
13
|
+
readdirSync: vi.fn(() => []),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('analyzeFile', () => {
|
|
18
|
+
it('is a function', () => {
|
|
19
|
+
expect(typeof analyzeFile).toBe('function');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('does not throw when called with a valid path string', () => {
|
|
23
|
+
expect(() => analyzeFile('/fake/path/file.js')).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('does not throw when called with null', () => {
|
|
27
|
+
expect(() => analyzeFile(null)).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not throw when called with undefined', () => {
|
|
31
|
+
expect(() => analyzeFile(undefined)).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('does not throw when called with an empty string', () => {
|
|
35
|
+
expect(() => analyzeFile('')).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns a value', () => {
|
|
39
|
+
const result = analyzeFile('/fake/path/file.js');
|
|
40
|
+
expect(result).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns an object or null', () => {
|
|
44
|
+
const result = analyzeFile('/fake/path/file.js');
|
|
45
|
+
expect(result === null || typeof result === 'object').toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('analyzeProject', () => {
|
|
50
|
+
it('is a function', () => {
|
|
51
|
+
expect(typeof analyzeProject).toBe('function');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not throw when called with a valid path string', () => {
|
|
55
|
+
expect(() => analyzeProject('/fake/project')).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not throw when called with null', () => {
|
|
59
|
+
expect(() => analyzeProject(null)).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not throw when called with undefined', () => {
|
|
63
|
+
expect(() => analyzeProject(undefined)).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not throw when called with options', () => {
|
|
67
|
+
expect(() => analyzeProject('/fake/project', { include: ['**/*.js'], exclude: [] })).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns a value', () => {
|
|
71
|
+
const result = analyzeProject('/fake/project');
|
|
72
|
+
expect(result).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('extractSymbols', () => {
|
|
77
|
+
it('is a function', () => {
|
|
78
|
+
expect(typeof extractSymbols).toBe('function');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not throw when called with a file path', () => {
|
|
82
|
+
expect(() => extractSymbols('/fake/path/file.js')).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not throw when called with null', () => {
|
|
86
|
+
expect(() => extractSymbols(null)).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not throw when called with undefined', () => {
|
|
90
|
+
expect(() => extractSymbols(undefined)).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not throw when called with source content string', () => {
|
|
94
|
+
expect(() => extractSymbols('/fake/file.js', 'function hello() {}')).not.toThrow();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns a value', () => {
|
|
98
|
+
const result = extractSymbols('/fake/path/file.js');
|
|
99
|
+
expect(result).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('buildDependencyGraph', () => {
|
|
104
|
+
it('is a function', () => {
|
|
105
|
+
expect(typeof buildDependencyGraph).toBe('function');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('does not throw when called with an empty array', () => {
|
|
109
|
+
expect(() => buildDependencyGraph([])).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('does not throw when called with null', () => {
|
|
113
|
+
expect(() => buildDependencyGraph(null)).not.toThrow();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not throw when called with undefined', () => {
|
|
117
|
+
expect(() => buildDependencyGraph(undefined)).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not throw when called with an array of file paths', () => {
|
|
121
|
+
expect(() => buildDependencyGraph(['/fake/a.js', '/fake/b.js'])).not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns a value', () => {
|
|
125
|
+
const result = buildDependencyGraph([]);
|
|
126
|
+
expect(result).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
});
|