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.
@@ -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
+ };