codescoop 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/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/codescoop.js +276 -0
- package/package.json +75 -0
- package/src/cli/interactive.js +153 -0
- package/src/index.js +303 -0
- package/src/output/conversion-generator.js +501 -0
- package/src/output/markdown.js +562 -0
- package/src/parsers/css-analyzer.js +488 -0
- package/src/parsers/html-parser.js +455 -0
- package/src/parsers/js-analyzer.js +413 -0
- package/src/utils/file-scanner.js +191 -0
- package/src/utils/ghost-detector.js +174 -0
- package/src/utils/library-detector.js +335 -0
- package/src/utils/specificity-calculator.js +251 -0
- package/src/utils/template-parser.js +260 -0
- package/src/utils/url-fetcher.js +123 -0
- package/src/utils/validation.js +278 -0
- package/src/utils/variable-extractor.js +271 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript Analyzer Module
|
|
3
|
+
* Finds JS code that references the target element
|
|
4
|
+
* Supports: .js, .min.js, handles minified code
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const acorn = require('acorn');
|
|
10
|
+
const acornLoose = require('acorn-loose');
|
|
11
|
+
const walk = require('acorn-walk');
|
|
12
|
+
const beautify = require('js-beautify').js;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Analyze a JavaScript file for references to the target element
|
|
16
|
+
* @param {string} filePath - Path to JS file
|
|
17
|
+
* @param {Object} targetInfo - Target element information
|
|
18
|
+
* @param {Object} options - Options
|
|
19
|
+
* @returns {Object} Analysis result
|
|
20
|
+
*/
|
|
21
|
+
async function analyzeJS(filePath, targetInfo, options = {}) {
|
|
22
|
+
const { verbose = false } = options;
|
|
23
|
+
const log = verbose ? console.log : () => { };
|
|
24
|
+
|
|
25
|
+
let content;
|
|
26
|
+
let originalContent;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
30
|
+
content = originalContent;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
log(` Warning: Could not read ${filePath}: ${error.message}`);
|
|
33
|
+
return { filePath, matches: [], error: error.message };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Detect if minified
|
|
37
|
+
const isMinified = detectMinified(content);
|
|
38
|
+
|
|
39
|
+
// If minified, beautify for better analysis
|
|
40
|
+
let beautifiedContent = content;
|
|
41
|
+
if (isMinified) {
|
|
42
|
+
try {
|
|
43
|
+
beautifiedContent = beautify(content, {
|
|
44
|
+
indent_size: 2,
|
|
45
|
+
space_in_empty_paren: true
|
|
46
|
+
});
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// If beautification fails, continue with original
|
|
49
|
+
log(` Warning: Could not beautify ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const matches = [];
|
|
54
|
+
|
|
55
|
+
// Build search patterns
|
|
56
|
+
const patterns = buildSearchPatterns(targetInfo);
|
|
57
|
+
|
|
58
|
+
// Method 1: AST-based analysis (more accurate)
|
|
59
|
+
try {
|
|
60
|
+
const astMatches = analyzeWithAST(beautifiedContent, patterns, targetInfo);
|
|
61
|
+
matches.push(...astMatches);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
log(` AST parsing failed for ${filePath}, falling back to regex`);
|
|
64
|
+
// Method 2: Regex-based fallback (for malformed JS)
|
|
65
|
+
const regexMatches = analyzeWithRegex(beautifiedContent, patterns, targetInfo);
|
|
66
|
+
matches.push(...regexMatches);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Deduplicate matches
|
|
70
|
+
const uniqueMatches = deduplicateMatches(matches);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
filePath,
|
|
74
|
+
relativePath: path.relative(process.cwd(), filePath),
|
|
75
|
+
matches: uniqueMatches,
|
|
76
|
+
isMinified,
|
|
77
|
+
wasBeautified: isMinified,
|
|
78
|
+
originalContent: isMinified ? originalContent : null,
|
|
79
|
+
beautifiedContent: isMinified ? beautifiedContent : null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build search patterns for the target element
|
|
85
|
+
*/
|
|
86
|
+
function buildSearchPatterns(targetInfo) {
|
|
87
|
+
const patterns = [];
|
|
88
|
+
|
|
89
|
+
// Class patterns
|
|
90
|
+
targetInfo.classes.forEach(cls => {
|
|
91
|
+
patterns.push({
|
|
92
|
+
type: 'class',
|
|
93
|
+
value: cls,
|
|
94
|
+
// Various ways classes are referenced in JS
|
|
95
|
+
regexPatterns: [
|
|
96
|
+
new RegExp(`['"]\\.${escapeRegex(cls)}['"]`, 'g'), // '.class'
|
|
97
|
+
new RegExp(`['"]${escapeRegex(cls)}['"]`, 'g'), // 'class' (for classList)
|
|
98
|
+
new RegExp(`\\.${escapeRegex(cls)}(?=[\\s'"\\]])`, 'g'), // .class in selectors
|
|
99
|
+
],
|
|
100
|
+
stringPatterns: [
|
|
101
|
+
`.${cls}`,
|
|
102
|
+
`'${cls}'`,
|
|
103
|
+
`"${cls}"`,
|
|
104
|
+
`\`${cls}\``
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ID patterns
|
|
110
|
+
targetInfo.ids.forEach(id => {
|
|
111
|
+
patterns.push({
|
|
112
|
+
type: 'id',
|
|
113
|
+
value: id,
|
|
114
|
+
regexPatterns: [
|
|
115
|
+
new RegExp(`['"]#${escapeRegex(id)}['"]`, 'g'), // '#id'
|
|
116
|
+
new RegExp(`getElementById\\s*\\(\\s*['"]${escapeRegex(id)}['"]`, 'g'),
|
|
117
|
+
],
|
|
118
|
+
stringPatterns: [
|
|
119
|
+
`#${id}`,
|
|
120
|
+
`'${id}'`,
|
|
121
|
+
`"${id}"`,
|
|
122
|
+
`getElementById('${id}')`,
|
|
123
|
+
`getElementById("${id}")`
|
|
124
|
+
]
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Data attribute patterns
|
|
129
|
+
targetInfo.dataAttributes.forEach(attr => {
|
|
130
|
+
patterns.push({
|
|
131
|
+
type: 'data-attr',
|
|
132
|
+
value: attr,
|
|
133
|
+
regexPatterns: [
|
|
134
|
+
new RegExp(`['"]\\[${escapeRegex(attr)}`, 'g'),
|
|
135
|
+
new RegExp(`dataset\\.${escapeRegex(attr.replace('data-', '').replace(/-([a-z])/g, (_, l) => l.toUpperCase()))}`, 'g'),
|
|
136
|
+
],
|
|
137
|
+
stringPatterns: [
|
|
138
|
+
`[${attr}]`,
|
|
139
|
+
attr
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return patterns;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Analyze JS using AST parsing
|
|
149
|
+
*/
|
|
150
|
+
function analyzeWithAST(content, patterns, targetInfo) {
|
|
151
|
+
const matches = [];
|
|
152
|
+
const lines = content.split('\n');
|
|
153
|
+
|
|
154
|
+
// Parse with acorn, falling back to loose parsing
|
|
155
|
+
let ast;
|
|
156
|
+
try {
|
|
157
|
+
ast = acorn.parse(content, {
|
|
158
|
+
ecmaVersion: 'latest',
|
|
159
|
+
sourceType: 'module',
|
|
160
|
+
locations: true,
|
|
161
|
+
allowHashBang: true,
|
|
162
|
+
allowReserved: true
|
|
163
|
+
});
|
|
164
|
+
} catch (e) {
|
|
165
|
+
ast = acornLoose.parse(content, {
|
|
166
|
+
ecmaVersion: 'latest',
|
|
167
|
+
sourceType: 'module',
|
|
168
|
+
locations: true
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Walk the AST looking for relevant patterns
|
|
173
|
+
walk.simple(ast, {
|
|
174
|
+
// Look for string literals
|
|
175
|
+
Literal(node) {
|
|
176
|
+
if (typeof node.value !== 'string') return;
|
|
177
|
+
|
|
178
|
+
const match = checkStringMatch(node.value, patterns);
|
|
179
|
+
if (match) {
|
|
180
|
+
const startLine = node.loc?.start?.line || 1;
|
|
181
|
+
const endLine = node.loc?.end?.line || startLine;
|
|
182
|
+
|
|
183
|
+
// Get surrounding context (the statement containing this literal)
|
|
184
|
+
const contextLines = getContextLines(lines, startLine - 1, 2);
|
|
185
|
+
|
|
186
|
+
matches.push({
|
|
187
|
+
type: 'string-literal',
|
|
188
|
+
matchedOn: match.matchedOn,
|
|
189
|
+
content: contextLines.content,
|
|
190
|
+
startLine: contextLines.startLine,
|
|
191
|
+
endLine: contextLines.endLine,
|
|
192
|
+
value: node.value
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Look for template literals
|
|
198
|
+
TemplateLiteral(node) {
|
|
199
|
+
// Get the full template string
|
|
200
|
+
const quasis = node.quasis.map(q => q.value.raw).join('');
|
|
201
|
+
|
|
202
|
+
const match = checkStringMatch(quasis, patterns);
|
|
203
|
+
if (match) {
|
|
204
|
+
const startLine = node.loc?.start?.line || 1;
|
|
205
|
+
const endLine = node.loc?.end?.line || startLine;
|
|
206
|
+
|
|
207
|
+
const contextLines = getContextLines(lines, startLine - 1, 2);
|
|
208
|
+
|
|
209
|
+
matches.push({
|
|
210
|
+
type: 'template-literal',
|
|
211
|
+
matchedOn: match.matchedOn,
|
|
212
|
+
content: contextLines.content,
|
|
213
|
+
startLine: contextLines.startLine,
|
|
214
|
+
endLine: contextLines.endLine
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// Look for querySelector, getElementById, etc.
|
|
220
|
+
CallExpression(node) {
|
|
221
|
+
if (node.callee.type === 'MemberExpression') {
|
|
222
|
+
const methodName = node.callee.property?.name;
|
|
223
|
+
|
|
224
|
+
const selectorMethods = [
|
|
225
|
+
'querySelector', 'querySelectorAll',
|
|
226
|
+
'getElementById', 'getElementsByClassName',
|
|
227
|
+
'getElementsByTagName', 'closest', 'matches'
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// jQuery-style selectors
|
|
231
|
+
const jQueryMethods = ['find', 'children', 'parent', 'parents', 'siblings'];
|
|
232
|
+
|
|
233
|
+
if (selectorMethods.includes(methodName) || jQueryMethods.includes(methodName)) {
|
|
234
|
+
// Check the first argument
|
|
235
|
+
const firstArg = node.arguments[0];
|
|
236
|
+
if (firstArg && (firstArg.type === 'Literal' || firstArg.type === 'TemplateLiteral')) {
|
|
237
|
+
const argValue = firstArg.type === 'Literal'
|
|
238
|
+
? firstArg.value
|
|
239
|
+
: firstArg.quasis?.map(q => q.value.raw).join('');
|
|
240
|
+
|
|
241
|
+
if (typeof argValue === 'string') {
|
|
242
|
+
const match = checkStringMatch(argValue, patterns);
|
|
243
|
+
if (match) {
|
|
244
|
+
const startLine = node.loc?.start?.line || 1;
|
|
245
|
+
const endLine = node.loc?.end?.line || startLine;
|
|
246
|
+
|
|
247
|
+
const contextLines = getContextLines(lines, startLine - 1, 3);
|
|
248
|
+
|
|
249
|
+
matches.push({
|
|
250
|
+
type: 'dom-query',
|
|
251
|
+
method: methodName,
|
|
252
|
+
matchedOn: match.matchedOn,
|
|
253
|
+
content: contextLines.content,
|
|
254
|
+
startLine: contextLines.startLine,
|
|
255
|
+
endLine: contextLines.endLine,
|
|
256
|
+
selector: argValue
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check for jQuery $() calls
|
|
265
|
+
if (node.callee.name === '$' || node.callee.name === 'jQuery') {
|
|
266
|
+
const firstArg = node.arguments[0];
|
|
267
|
+
if (firstArg && firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
268
|
+
const match = checkStringMatch(firstArg.value, patterns);
|
|
269
|
+
if (match) {
|
|
270
|
+
const startLine = node.loc?.start?.line || 1;
|
|
271
|
+
const endLine = node.loc?.end?.line || startLine;
|
|
272
|
+
|
|
273
|
+
const contextLines = getContextLines(lines, startLine - 1, 3);
|
|
274
|
+
|
|
275
|
+
matches.push({
|
|
276
|
+
type: 'jquery',
|
|
277
|
+
matchedOn: match.matchedOn,
|
|
278
|
+
content: contextLines.content,
|
|
279
|
+
startLine: contextLines.startLine,
|
|
280
|
+
endLine: contextLines.endLine,
|
|
281
|
+
selector: firstArg.value
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Look for classList operations
|
|
289
|
+
MemberExpression(node) {
|
|
290
|
+
if (node.property?.name === 'classList') {
|
|
291
|
+
// This is accessing classList, the actual class name will be in the parent call
|
|
292
|
+
// This is handled by CallExpression above
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return matches;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Fallback regex-based analysis for malformed JS
|
|
302
|
+
*/
|
|
303
|
+
function analyzeWithRegex(content, patterns, targetInfo) {
|
|
304
|
+
const matches = [];
|
|
305
|
+
const lines = content.split('\n');
|
|
306
|
+
|
|
307
|
+
// Common DOM query patterns
|
|
308
|
+
const domPatterns = [
|
|
309
|
+
/document\.querySelector\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
310
|
+
/document\.querySelectorAll\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
311
|
+
/document\.getElementById\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
312
|
+
/document\.getElementsByClassName\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
313
|
+
/\$\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
314
|
+
/jQuery\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
// Search for each pattern
|
|
318
|
+
for (const pattern of patterns) {
|
|
319
|
+
for (const strPattern of pattern.stringPatterns) {
|
|
320
|
+
let lineNum = 0;
|
|
321
|
+
for (const line of lines) {
|
|
322
|
+
lineNum++;
|
|
323
|
+
if (line.includes(strPattern)) {
|
|
324
|
+
const contextLines = getContextLines(lines, lineNum - 1, 2);
|
|
325
|
+
|
|
326
|
+
matches.push({
|
|
327
|
+
type: 'regex-match',
|
|
328
|
+
matchedOn: [`${pattern.type}: ${pattern.value}`],
|
|
329
|
+
content: contextLines.content,
|
|
330
|
+
startLine: contextLines.startLine,
|
|
331
|
+
endLine: contextLines.endLine
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return matches;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if a string matches any of our patterns
|
|
343
|
+
*/
|
|
344
|
+
function checkStringMatch(str, patterns) {
|
|
345
|
+
const matchedOn = [];
|
|
346
|
+
|
|
347
|
+
for (const pattern of patterns) {
|
|
348
|
+
for (const strPattern of pattern.stringPatterns) {
|
|
349
|
+
if (str.includes(strPattern) || str === pattern.value) {
|
|
350
|
+
matchedOn.push(`${pattern.type}: ${pattern.value}`);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return matchedOn.length > 0 ? { matchedOn } : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get context lines around a match
|
|
361
|
+
*/
|
|
362
|
+
function getContextLines(lines, centerIndex, contextSize) {
|
|
363
|
+
const startIndex = Math.max(0, centerIndex - contextSize);
|
|
364
|
+
const endIndex = Math.min(lines.length - 1, centerIndex + contextSize);
|
|
365
|
+
|
|
366
|
+
const contextLines = lines.slice(startIndex, endIndex + 1);
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
content: contextLines.join('\n'),
|
|
370
|
+
startLine: startIndex + 1,
|
|
371
|
+
endLine: endIndex + 1
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Deduplicate matches based on content
|
|
377
|
+
*/
|
|
378
|
+
function deduplicateMatches(matches) {
|
|
379
|
+
const seen = new Set();
|
|
380
|
+
return matches.filter(match => {
|
|
381
|
+
const key = `${match.startLine}-${match.endLine}-${match.content}`;
|
|
382
|
+
if (seen.has(key)) return false;
|
|
383
|
+
seen.add(key);
|
|
384
|
+
return true;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Detect if JS content is minified
|
|
390
|
+
*/
|
|
391
|
+
function detectMinified(content) {
|
|
392
|
+
const lines = content.split('\n');
|
|
393
|
+
if (lines.length === 0) return false;
|
|
394
|
+
|
|
395
|
+
const avgLineLength = content.length / lines.length;
|
|
396
|
+
const newlineRatio = lines.length / content.length;
|
|
397
|
+
|
|
398
|
+
// Also check for common minification patterns
|
|
399
|
+
const hasLongLines = lines.some(line => line.length > 500);
|
|
400
|
+
|
|
401
|
+
return avgLineLength > 200 || newlineRatio < 0.002 || hasLongLines;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Escape special regex characters
|
|
406
|
+
*/
|
|
407
|
+
function escapeRegex(string) {
|
|
408
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
module.exports = {
|
|
412
|
+
analyzeJS
|
|
413
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Scanner Utility
|
|
3
|
+
* Scans project directory for CSS/JS files
|
|
4
|
+
* Also identifies which files are linked in HTML
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { glob } = require('glob');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find all CSS and JS files in a project directory
|
|
13
|
+
* @param {string} projectDir - Project directory path
|
|
14
|
+
* @returns {Object} Object with css and js file arrays
|
|
15
|
+
*/
|
|
16
|
+
async function findProjectFiles(projectDir) {
|
|
17
|
+
const cssPatterns = [
|
|
18
|
+
'**/*.css',
|
|
19
|
+
'**/*.scss',
|
|
20
|
+
'**/*.sass',
|
|
21
|
+
'**/*.less'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const jsPatterns = [
|
|
25
|
+
'**/*.js',
|
|
26
|
+
'**/*.mjs',
|
|
27
|
+
'**/*.cjs'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const ignorePatterns = [
|
|
31
|
+
'**/node_modules/**',
|
|
32
|
+
'**/bower_components/**',
|
|
33
|
+
'**/vendor/**',
|
|
34
|
+
'**/.git/**',
|
|
35
|
+
'**/dist/**',
|
|
36
|
+
'**/build/**',
|
|
37
|
+
'**/coverage/**',
|
|
38
|
+
'**/*.min.js.map',
|
|
39
|
+
'**/*.min.css.map'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const globOptions = {
|
|
43
|
+
cwd: projectDir,
|
|
44
|
+
ignore: ignorePatterns,
|
|
45
|
+
absolute: true,
|
|
46
|
+
nodir: true
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Find CSS files
|
|
50
|
+
const cssPromises = cssPatterns.map(pattern =>
|
|
51
|
+
glob(pattern, globOptions)
|
|
52
|
+
);
|
|
53
|
+
const cssResults = await Promise.all(cssPromises);
|
|
54
|
+
const cssFiles = [...new Set(cssResults.flat())];
|
|
55
|
+
|
|
56
|
+
// Find JS files
|
|
57
|
+
const jsPromises = jsPatterns.map(pattern =>
|
|
58
|
+
glob(pattern, globOptions)
|
|
59
|
+
);
|
|
60
|
+
const jsResults = await Promise.all(jsPromises);
|
|
61
|
+
const jsFiles = [...new Set(jsResults.flat())];
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
css: cssFiles,
|
|
65
|
+
js: jsFiles
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get files that are actually linked in the HTML
|
|
71
|
+
* @param {CheerioAPI} $ - Cheerio instance with parsed HTML
|
|
72
|
+
* @param {string} htmlPath - Path to the HTML file (for resolving relative paths)
|
|
73
|
+
* @returns {Object} Object with linked css and js file arrays
|
|
74
|
+
*/
|
|
75
|
+
function getLinkedFiles($, htmlPath) {
|
|
76
|
+
const htmlDir = path.dirname(htmlPath);
|
|
77
|
+
const linkedCSS = [];
|
|
78
|
+
const linkedJS = [];
|
|
79
|
+
|
|
80
|
+
// Find linked stylesheets
|
|
81
|
+
$('link[rel="stylesheet"]').each((_, element) => {
|
|
82
|
+
const href = $(element).attr('href');
|
|
83
|
+
if (href && !href.startsWith('http') && !href.startsWith('//')) {
|
|
84
|
+
const resolved = resolvePath(href, htmlDir);
|
|
85
|
+
if (resolved) linkedCSS.push(resolved);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Find CSS @import in style tags
|
|
90
|
+
$('style').each((_, element) => {
|
|
91
|
+
const content = $(element).html() || '';
|
|
92
|
+
const importMatches = content.matchAll(/@import\s+(?:url\()?['"]?([^'"\)]+)['"]?\)?/g);
|
|
93
|
+
for (const match of importMatches) {
|
|
94
|
+
const href = match[1];
|
|
95
|
+
if (href && !href.startsWith('http') && !href.startsWith('//')) {
|
|
96
|
+
const resolved = resolvePath(href, htmlDir);
|
|
97
|
+
if (resolved) linkedCSS.push(resolved);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Find script sources
|
|
103
|
+
$('script[src]').each((_, element) => {
|
|
104
|
+
const src = $(element).attr('src');
|
|
105
|
+
if (src && !src.startsWith('http') && !src.startsWith('//')) {
|
|
106
|
+
const resolved = resolvePath(src, htmlDir);
|
|
107
|
+
if (resolved) linkedJS.push(resolved);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
css: [...new Set(linkedCSS)],
|
|
113
|
+
js: [...new Set(linkedJS)]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a relative path from HTML file location
|
|
119
|
+
*/
|
|
120
|
+
function resolvePath(relativePath, htmlDir) {
|
|
121
|
+
try {
|
|
122
|
+
// Handle paths starting with /
|
|
123
|
+
if (relativePath.startsWith('/')) {
|
|
124
|
+
// Assume it's relative to project root - try to find the file
|
|
125
|
+
// This is a simplification; in real projects you might need more logic
|
|
126
|
+
relativePath = relativePath.substring(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Remove query strings and hashes
|
|
130
|
+
relativePath = relativePath.split('?')[0].split('#')[0];
|
|
131
|
+
|
|
132
|
+
const resolved = path.resolve(htmlDir, relativePath);
|
|
133
|
+
|
|
134
|
+
// Check if file exists
|
|
135
|
+
if (fs.existsSync(resolved)) {
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Try without leading dots
|
|
140
|
+
const altPath = path.resolve(htmlDir, relativePath.replace(/^\.\//, ''));
|
|
141
|
+
if (fs.existsSync(altPath)) {
|
|
142
|
+
return altPath;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get file info for display
|
|
153
|
+
*/
|
|
154
|
+
function getFileInfo(filePath) {
|
|
155
|
+
try {
|
|
156
|
+
const stats = fs.statSync(filePath);
|
|
157
|
+
return {
|
|
158
|
+
path: filePath,
|
|
159
|
+
baseName: path.basename(filePath),
|
|
160
|
+
extension: path.extname(filePath),
|
|
161
|
+
size: stats.size,
|
|
162
|
+
sizeFormatted: formatFileSize(stats.size)
|
|
163
|
+
};
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
path: filePath,
|
|
167
|
+
baseName: path.basename(filePath),
|
|
168
|
+
extension: path.extname(filePath),
|
|
169
|
+
size: 0,
|
|
170
|
+
sizeFormatted: 'N/A'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Format file size for display
|
|
177
|
+
*/
|
|
178
|
+
function formatFileSize(bytes) {
|
|
179
|
+
if (bytes === 0) return '0 B';
|
|
180
|
+
const k = 1024;
|
|
181
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
182
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
183
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
findProjectFiles,
|
|
188
|
+
getLinkedFiles,
|
|
189
|
+
getFileInfo,
|
|
190
|
+
formatFileSize
|
|
191
|
+
};
|