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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive CLI Mode
|
|
3
|
+
* Shows HTML structure and lets user select a component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { parseHTML, getHTMLStructure } = require('../parsers/html-parser');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run interactive mode to select a component
|
|
13
|
+
* @param {string} htmlPath - Path to HTML file
|
|
14
|
+
* @returns {Object|null} Selected component info or null if cancelled
|
|
15
|
+
*/
|
|
16
|
+
async function runInteractiveMode(htmlPath) {
|
|
17
|
+
// Parse HTML file
|
|
18
|
+
const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
|
|
19
|
+
const $ = parseHTML(htmlContent);
|
|
20
|
+
|
|
21
|
+
// Get structure
|
|
22
|
+
const structure = getHTMLStructure($);
|
|
23
|
+
|
|
24
|
+
if (structure.length === 0) {
|
|
25
|
+
console.log(chalk.yellow('No significant HTML elements found in the file.'));
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Display structure
|
|
30
|
+
console.log(chalk.cyan('┌─────────────────────────────────────────────────────────────┐'));
|
|
31
|
+
console.log(chalk.cyan('│') + chalk.bold.white(' HTML Structure ') + chalk.cyan('│'));
|
|
32
|
+
console.log(chalk.cyan('├─────────────────────────────────────────────────────────────┤'));
|
|
33
|
+
|
|
34
|
+
structure.forEach(item => {
|
|
35
|
+
const indexStr = chalk.gray(`[${String(item.index).padStart(2)}]`);
|
|
36
|
+
const display = item.isChild
|
|
37
|
+
? chalk.gray(item.display)
|
|
38
|
+
: chalk.white(item.display);
|
|
39
|
+
console.log(chalk.cyan('│') + ` ${indexStr} ${display}`.padEnd(60) + chalk.cyan('│'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan('└─────────────────────────────────────────────────────────────┘'));
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Build choices for inquirer
|
|
46
|
+
const choices = structure.map(item => ({
|
|
47
|
+
name: `${item.display}`,
|
|
48
|
+
value: item.selector,
|
|
49
|
+
short: item.selector
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Add option to enter custom selector
|
|
53
|
+
choices.push(new inquirer.Separator());
|
|
54
|
+
choices.push({
|
|
55
|
+
name: chalk.yellow('Enter custom selector...'),
|
|
56
|
+
value: '__custom__'
|
|
57
|
+
});
|
|
58
|
+
choices.push({
|
|
59
|
+
name: chalk.gray('Cancel'),
|
|
60
|
+
value: '__cancel__'
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Prompt for selection
|
|
64
|
+
const { selection } = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: 'list',
|
|
67
|
+
name: 'selection',
|
|
68
|
+
message: 'Select a component to analyze:',
|
|
69
|
+
choices,
|
|
70
|
+
pageSize: 15
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
if (selection === '__cancel__') {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (selection === '__custom__') {
|
|
79
|
+
const { customSelector } = await inquirer.prompt([
|
|
80
|
+
{
|
|
81
|
+
type: 'input',
|
|
82
|
+
name: 'customSelector',
|
|
83
|
+
message: 'Enter CSS selector:',
|
|
84
|
+
validate: (input) => {
|
|
85
|
+
if (!input.trim()) {
|
|
86
|
+
return 'Please enter a valid CSS selector';
|
|
87
|
+
}
|
|
88
|
+
// Basic validation - try to use it
|
|
89
|
+
try {
|
|
90
|
+
$(input);
|
|
91
|
+
return true;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return 'Invalid CSS selector';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
selector: customSelector,
|
|
101
|
+
mode: 'custom'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
selector: selection,
|
|
107
|
+
mode: 'selected'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Display a preview of what will be analyzed
|
|
113
|
+
*/
|
|
114
|
+
async function showPreview($, selector) {
|
|
115
|
+
const element = $(selector).first();
|
|
116
|
+
|
|
117
|
+
if (element.length === 0) {
|
|
118
|
+
console.log(chalk.red(`No element found matching: ${selector}`));
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const classes = element.attr('class')?.split(/\s+/).filter(c => c) || [];
|
|
123
|
+
const id = element.attr('id');
|
|
124
|
+
const tagName = element.prop('tagName')?.toLowerCase();
|
|
125
|
+
const childCount = element.find('*').length;
|
|
126
|
+
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(chalk.cyan('Component Preview:'));
|
|
129
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
130
|
+
console.log(` Tag: ${chalk.white(tagName)}`);
|
|
131
|
+
if (id) console.log(` ID: ${chalk.yellow('#' + id)}`);
|
|
132
|
+
if (classes.length > 0) console.log(` Classes: ${chalk.green(classes.map(c => '.' + c).join(' '))}`);
|
|
133
|
+
console.log(` Children: ${chalk.gray(childCount + ' elements')}`);
|
|
134
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
135
|
+
console.log('');
|
|
136
|
+
|
|
137
|
+
// Confirm
|
|
138
|
+
const { confirmed } = await inquirer.prompt([
|
|
139
|
+
{
|
|
140
|
+
type: 'confirm',
|
|
141
|
+
name: 'confirmed',
|
|
142
|
+
message: 'Analyze this component?',
|
|
143
|
+
default: true
|
|
144
|
+
}
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
return confirmed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
runInteractiveMode,
|
|
152
|
+
showPreview
|
|
153
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main orchestrator for html-scan
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { parseHTML, extractTargetElement } = require('./parsers/html-parser');
|
|
8
|
+
const { analyzeCSS } = require('./parsers/css-analyzer');
|
|
9
|
+
const { analyzeJS } = require('./parsers/js-analyzer');
|
|
10
|
+
const { findProjectFiles, getLinkedFiles } = require('./utils/file-scanner');
|
|
11
|
+
const { generateMarkdown } = require('./output/markdown');
|
|
12
|
+
const {
|
|
13
|
+
detectLibrariesFromPaths,
|
|
14
|
+
detectLibrariesFromHTML,
|
|
15
|
+
detectLibrariesFromClasses,
|
|
16
|
+
isLibraryFile
|
|
17
|
+
} = require('./utils/library-detector');
|
|
18
|
+
const { extractVariablesFromMatches } = require('./utils/variable-extractor');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run the full analysis
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {string} options.htmlPath - Path to HTML file
|
|
24
|
+
* @param {string} options.projectDir - Project directory to scan
|
|
25
|
+
* @param {string} options.selector - CSS selector to target
|
|
26
|
+
* @param {string} options.lineRange - Line range (e.g., "45-80")
|
|
27
|
+
* @param {string} options.outputPath - Output file path
|
|
28
|
+
* @param {boolean} options.includeInline - Include inline styles/scripts
|
|
29
|
+
* @param {boolean} options.verbose - Verbose logging
|
|
30
|
+
*/
|
|
31
|
+
async function runAnalysis(options) {
|
|
32
|
+
const {
|
|
33
|
+
htmlPath,
|
|
34
|
+
projectDir,
|
|
35
|
+
selector,
|
|
36
|
+
lineRange,
|
|
37
|
+
matchIndex = 0,
|
|
38
|
+
outputPath,
|
|
39
|
+
includeInline = true,
|
|
40
|
+
verbose = false,
|
|
41
|
+
// Compact mode options
|
|
42
|
+
compact = false,
|
|
43
|
+
forConversion = false,
|
|
44
|
+
maxRulesPerFile = 20,
|
|
45
|
+
maxJsPerFile = 10,
|
|
46
|
+
summaryOnly = false,
|
|
47
|
+
skipMinified = false,
|
|
48
|
+
// URL/Template mode support
|
|
49
|
+
htmlContent: preloadedContent = null,
|
|
50
|
+
sourceType = 'file'
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
const log = verbose ? console.log : () => { };
|
|
54
|
+
|
|
55
|
+
// Step 1: Parse HTML and extract target element
|
|
56
|
+
log('Parsing HTML file...');
|
|
57
|
+
|
|
58
|
+
// Use pre-loaded content (from URL/template) or read from file
|
|
59
|
+
let htmlContent;
|
|
60
|
+
if (preloadedContent) {
|
|
61
|
+
htmlContent = preloadedContent;
|
|
62
|
+
log(`Using pre-loaded HTML content (${sourceType} mode)`);
|
|
63
|
+
} else {
|
|
64
|
+
htmlContent = fs.readFileSync(htmlPath, 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const parsedHTML = parseHTML(htmlContent);
|
|
68
|
+
|
|
69
|
+
const targetInfo = extractTargetElement(parsedHTML, htmlContent, {
|
|
70
|
+
selector,
|
|
71
|
+
lineRange,
|
|
72
|
+
matchIndex
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
log(`Target element: ${targetInfo.summary}`);
|
|
76
|
+
log(`Classes: ${targetInfo.classes.join(', ') || 'none'}`);
|
|
77
|
+
log(`IDs: ${targetInfo.ids.join(', ') || 'none'}`);
|
|
78
|
+
|
|
79
|
+
// Step 2: Find all CSS/JS files in project (skip if no projectDir for URL mode)
|
|
80
|
+
log('\nScanning project for CSS/JS files...');
|
|
81
|
+
let projectFiles = { css: [], js: [] };
|
|
82
|
+
if (projectDir) {
|
|
83
|
+
projectFiles = await findProjectFiles(projectDir);
|
|
84
|
+
}
|
|
85
|
+
log(`Found ${projectFiles.css.length} CSS files, ${projectFiles.js.length} JS files`);
|
|
86
|
+
|
|
87
|
+
// Step 3: Get files that are actually linked in HTML
|
|
88
|
+
const linkedFiles = getLinkedFiles(parsedHTML, htmlPath);
|
|
89
|
+
log(`Linked in HTML: ${linkedFiles.css.length} CSS, ${linkedFiles.js.length} JS`);
|
|
90
|
+
|
|
91
|
+
// Step 4: Detect libraries
|
|
92
|
+
log('\nDetecting libraries...');
|
|
93
|
+
const allFiles = [...projectFiles.css, ...projectFiles.js];
|
|
94
|
+
const librariesFromFiles = detectLibrariesFromPaths(allFiles);
|
|
95
|
+
const librariesFromCDN = detectLibrariesFromHTML(parsedHTML);
|
|
96
|
+
const librariesFromClasses = detectLibrariesFromClasses(targetInfo.classes);
|
|
97
|
+
|
|
98
|
+
// Combine library info
|
|
99
|
+
const detectedLibraries = {
|
|
100
|
+
fromFiles: librariesFromFiles,
|
|
101
|
+
fromCDN: librariesFromCDN,
|
|
102
|
+
fromClasses: librariesFromClasses
|
|
103
|
+
};
|
|
104
|
+
log(`Detected ${Object.keys(librariesFromFiles).length} libraries from files`);
|
|
105
|
+
log(`Detected ${librariesFromCDN.length} libraries from CDN`);
|
|
106
|
+
|
|
107
|
+
// Step 5: Analyze all CSS files (separating libraries from custom code)
|
|
108
|
+
log('\nAnalyzing CSS files...');
|
|
109
|
+
const cssResults = [];
|
|
110
|
+
const cssLibraryResults = [];
|
|
111
|
+
|
|
112
|
+
for (const cssFile of projectFiles.css) {
|
|
113
|
+
const libInfo = isLibraryFile(cssFile);
|
|
114
|
+
const result = await analyzeCSS(cssFile, targetInfo, { verbose });
|
|
115
|
+
|
|
116
|
+
if (result.matches.length > 0) {
|
|
117
|
+
result.isLinked = linkedFiles.css.some(f =>
|
|
118
|
+
path.resolve(f) === path.resolve(cssFile)
|
|
119
|
+
);
|
|
120
|
+
result.isLibrary = !!libInfo;
|
|
121
|
+
result.libraryName = libInfo?.name || null;
|
|
122
|
+
|
|
123
|
+
if (libInfo) {
|
|
124
|
+
cssLibraryResults.push(result);
|
|
125
|
+
} else {
|
|
126
|
+
cssResults.push(result);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Step 6: Analyze inline styles if requested
|
|
132
|
+
let inlineStyles = [];
|
|
133
|
+
if (includeInline) {
|
|
134
|
+
log('Extracting inline styles...');
|
|
135
|
+
inlineStyles = extractInlineStyles(parsedHTML, targetInfo);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Step 7: Analyze all JS files (separating libraries from custom code)
|
|
139
|
+
log('\nAnalyzing JavaScript files...');
|
|
140
|
+
const jsResults = [];
|
|
141
|
+
const jsLibraryResults = [];
|
|
142
|
+
|
|
143
|
+
for (const jsFile of projectFiles.js) {
|
|
144
|
+
const libInfo = isLibraryFile(jsFile);
|
|
145
|
+
const result = await analyzeJS(jsFile, targetInfo, { verbose });
|
|
146
|
+
|
|
147
|
+
if (result.matches.length > 0) {
|
|
148
|
+
result.isLinked = linkedFiles.js.some(f =>
|
|
149
|
+
path.resolve(f) === path.resolve(jsFile)
|
|
150
|
+
);
|
|
151
|
+
result.isLibrary = !!libInfo;
|
|
152
|
+
result.libraryName = libInfo?.name || null;
|
|
153
|
+
|
|
154
|
+
if (libInfo) {
|
|
155
|
+
jsLibraryResults.push(result);
|
|
156
|
+
} else {
|
|
157
|
+
jsResults.push(result);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Step 8: Analyze inline scripts if requested
|
|
163
|
+
let inlineScripts = [];
|
|
164
|
+
if (includeInline) {
|
|
165
|
+
log('Extracting inline scripts...');
|
|
166
|
+
inlineScripts = extractInlineScripts(parsedHTML, targetInfo);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Step 9: Identify missing imports (only for custom files, not libraries)
|
|
170
|
+
const missingImports = [
|
|
171
|
+
...cssResults.filter(r => !r.isLinked).map(r => r.filePath),
|
|
172
|
+
...jsResults.filter(r => !r.isLinked).map(r => r.filePath)
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
// Step 10: Extract CSS/SCSS variable definitions
|
|
176
|
+
log('Extracting variable definitions...');
|
|
177
|
+
const allCSSMatches = [
|
|
178
|
+
...cssResults.flatMap(r => r.matches || []),
|
|
179
|
+
...inlineStyles.map(s => ({ content: s.content }))
|
|
180
|
+
];
|
|
181
|
+
const allCSSFiles = [...projectFiles.css];
|
|
182
|
+
const variableData = await extractVariablesFromMatches(allCSSMatches, allCSSFiles, projectDir);
|
|
183
|
+
log(`Found ${variableData.usedVariables.length} variables used`);
|
|
184
|
+
|
|
185
|
+
// Step 11: Generate markdown output
|
|
186
|
+
log('\nGenerating markdown report...');
|
|
187
|
+
const analysis = {
|
|
188
|
+
targetInfo,
|
|
189
|
+
htmlPath,
|
|
190
|
+
projectDir,
|
|
191
|
+
cssResults,
|
|
192
|
+
jsResults,
|
|
193
|
+
cssLibraryResults,
|
|
194
|
+
jsLibraryResults,
|
|
195
|
+
detectedLibraries,
|
|
196
|
+
inlineStyles,
|
|
197
|
+
inlineScripts,
|
|
198
|
+
missingImports,
|
|
199
|
+
variableData,
|
|
200
|
+
generatedAt: new Date().toISOString(),
|
|
201
|
+
// Output options
|
|
202
|
+
outputOptions: {
|
|
203
|
+
compact,
|
|
204
|
+
forConversion,
|
|
205
|
+
maxRulesPerFile,
|
|
206
|
+
maxJsPerFile,
|
|
207
|
+
summaryOnly,
|
|
208
|
+
skipMinified
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const markdown = generateMarkdown(analysis);
|
|
213
|
+
|
|
214
|
+
// Determine output path
|
|
215
|
+
const finalOutputPath = outputPath || generateOutputPath(targetInfo, htmlPath, projectDir);
|
|
216
|
+
fs.writeFileSync(finalOutputPath, markdown, 'utf-8');
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
outputPath: finalOutputPath,
|
|
220
|
+
cssMatches: cssResults.reduce((sum, r) => sum + r.matches.length, 0),
|
|
221
|
+
jsMatches: jsResults.reduce((sum, r) => sum + r.matches.length, 0),
|
|
222
|
+
missingImports,
|
|
223
|
+
libraryCount: Object.keys(librariesFromFiles).length + librariesFromCDN.length
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract inline <style> blocks that affect the target
|
|
229
|
+
*/
|
|
230
|
+
function extractInlineStyles(parsedHTML, targetInfo) {
|
|
231
|
+
const $ = parsedHTML;
|
|
232
|
+
const results = [];
|
|
233
|
+
|
|
234
|
+
$('style').each((index, element) => {
|
|
235
|
+
const content = $(element).html();
|
|
236
|
+
if (content) {
|
|
237
|
+
// Check if any target classes/IDs are referenced
|
|
238
|
+
const isRelevant = [...targetInfo.classes, ...targetInfo.ids, targetInfo.tagName]
|
|
239
|
+
.some(identifier => content.includes(identifier));
|
|
240
|
+
|
|
241
|
+
if (isRelevant) {
|
|
242
|
+
results.push({
|
|
243
|
+
index,
|
|
244
|
+
content: content.trim(),
|
|
245
|
+
type: 'inline-style'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Extract inline <script> blocks that reference the target
|
|
256
|
+
*/
|
|
257
|
+
function extractInlineScripts(parsedHTML, targetInfo) {
|
|
258
|
+
const $ = parsedHTML;
|
|
259
|
+
const results = [];
|
|
260
|
+
|
|
261
|
+
$('script:not([src])').each((index, element) => {
|
|
262
|
+
const content = $(element).html();
|
|
263
|
+
if (content) {
|
|
264
|
+
// Check if any target classes/IDs are referenced
|
|
265
|
+
const isRelevant = [...targetInfo.classes, ...targetInfo.ids]
|
|
266
|
+
.some(identifier => content.includes(identifier));
|
|
267
|
+
|
|
268
|
+
if (isRelevant) {
|
|
269
|
+
results.push({
|
|
270
|
+
index,
|
|
271
|
+
content: content.trim(),
|
|
272
|
+
type: 'inline-script'
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return results;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate output path based on target info
|
|
283
|
+
*/
|
|
284
|
+
function generateOutputPath(targetInfo, htmlPath, projectDir = null) {
|
|
285
|
+
// For URLs, use projectDir or current working directory
|
|
286
|
+
let outputDir;
|
|
287
|
+
if (htmlPath.startsWith('http://') || htmlPath.startsWith('https://')) {
|
|
288
|
+
outputDir = projectDir || process.cwd();
|
|
289
|
+
} else {
|
|
290
|
+
outputDir = path.dirname(htmlPath);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const baseName = targetInfo.ids[0]
|
|
294
|
+
|| targetInfo.classes[0]
|
|
295
|
+
|| targetInfo.tagName
|
|
296
|
+
|| 'component';
|
|
297
|
+
|
|
298
|
+
// Clean up the name for use as filename
|
|
299
|
+
const cleanName = baseName.replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
300
|
+
return path.join(outputDir, `${cleanName}-analysis.md`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = { runAnalysis };
|