codemeld 2.1.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/README.md +514 -0
- package/bin/cli.js +2 -0
- package/dist/ai/agent.d.ts +124 -0
- package/dist/ai/agent.d.ts.map +1 -0
- package/dist/ai/agent.js +289 -0
- package/dist/ai/agent.js.map +1 -0
- package/dist/ai/index.d.ts +10 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +10 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/prompts.d.ts +35 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +166 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/refinement-loop.d.ts +29 -0
- package/dist/ai/refinement-loop.d.ts.map +1 -0
- package/dist/ai/refinement-loop.js +180 -0
- package/dist/ai/refinement-loop.js.map +1 -0
- package/dist/ai/tools.d.ts +17 -0
- package/dist/ai/tools.d.ts.map +1 -0
- package/dist/ai/tools.js +353 -0
- package/dist/ai/tools.js.map +1 -0
- package/dist/ai/visual-compare.d.ts +43 -0
- package/dist/ai/visual-compare.d.ts.map +1 -0
- package/dist/ai/visual-compare.js +176 -0
- package/dist/ai/visual-compare.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +179 -0
- package/dist/cli.js.map +1 -0
- package/dist/converter.d.ts +10 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +836 -0
- package/dist/converter.js.map +1 -0
- package/dist/deconverter.d.ts +19 -0
- package/dist/deconverter.d.ts.map +1 -0
- package/dist/deconverter.js +188 -0
- package/dist/deconverter.js.map +1 -0
- package/dist/frameworks/angular-adapter.d.ts +27 -0
- package/dist/frameworks/angular-adapter.d.ts.map +1 -0
- package/dist/frameworks/angular-adapter.js +617 -0
- package/dist/frameworks/angular-adapter.js.map +1 -0
- package/dist/frameworks/index.d.ts +10 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +21 -0
- package/dist/frameworks/index.js.map +1 -0
- package/dist/frameworks/nextjs-adapter.d.ts +22 -0
- package/dist/frameworks/nextjs-adapter.d.ts.map +1 -0
- package/dist/frameworks/nextjs-adapter.js +392 -0
- package/dist/frameworks/nextjs-adapter.js.map +1 -0
- package/dist/frameworks/react-adapter.d.ts +21 -0
- package/dist/frameworks/react-adapter.d.ts.map +1 -0
- package/dist/frameworks/react-adapter.js +71 -0
- package/dist/frameworks/react-adapter.js.map +1 -0
- package/dist/frameworks/svelte-adapter.d.ts +27 -0
- package/dist/frameworks/svelte-adapter.d.ts.map +1 -0
- package/dist/frameworks/svelte-adapter.js +519 -0
- package/dist/frameworks/svelte-adapter.js.map +1 -0
- package/dist/frameworks/types.d.ts +78 -0
- package/dist/frameworks/types.d.ts.map +1 -0
- package/dist/frameworks/types.js +2 -0
- package/dist/frameworks/types.js.map +1 -0
- package/dist/frameworks/vue-adapter.d.ts +34 -0
- package/dist/frameworks/vue-adapter.d.ts.map +1 -0
- package/dist/frameworks/vue-adapter.js +632 -0
- package/dist/frameworks/vue-adapter.js.map +1 -0
- package/dist/generators/accessibility-generator.d.ts +43 -0
- package/dist/generators/accessibility-generator.d.ts.map +1 -0
- package/dist/generators/accessibility-generator.js +507 -0
- package/dist/generators/accessibility-generator.js.map +1 -0
- package/dist/generators/asset-handler.d.ts +14 -0
- package/dist/generators/asset-handler.d.ts.map +1 -0
- package/dist/generators/asset-handler.js +79 -0
- package/dist/generators/asset-handler.js.map +1 -0
- package/dist/generators/build-verifier.d.ts +8 -0
- package/dist/generators/build-verifier.d.ts.map +1 -0
- package/dist/generators/build-verifier.js +64 -0
- package/dist/generators/build-verifier.js.map +1 -0
- package/dist/generators/component-extractor.d.ts +25 -0
- package/dist/generators/component-extractor.d.ts.map +1 -0
- package/dist/generators/component-extractor.js +146 -0
- package/dist/generators/component-extractor.js.map +1 -0
- package/dist/generators/component-generator.d.ts +12 -0
- package/dist/generators/component-generator.d.ts.map +1 -0
- package/dist/generators/component-generator.js +724 -0
- package/dist/generators/component-generator.js.map +1 -0
- package/dist/generators/deploy-generator.d.ts +9 -0
- package/dist/generators/deploy-generator.d.ts.map +1 -0
- package/dist/generators/deploy-generator.js +409 -0
- package/dist/generators/deploy-generator.js.map +1 -0
- package/dist/generators/error-boundary.d.ts +5 -0
- package/dist/generators/error-boundary.d.ts.map +1 -0
- package/dist/generators/error-boundary.js +59 -0
- package/dist/generators/error-boundary.js.map +1 -0
- package/dist/generators/form-generator.d.ts +42 -0
- package/dist/generators/form-generator.d.ts.map +1 -0
- package/dist/generators/form-generator.js +662 -0
- package/dist/generators/form-generator.js.map +1 -0
- package/dist/generators/hooks-generator.d.ts +40 -0
- package/dist/generators/hooks-generator.d.ts.map +1 -0
- package/dist/generators/hooks-generator.js +297 -0
- package/dist/generators/hooks-generator.js.map +1 -0
- package/dist/generators/html-generator.d.ts +27 -0
- package/dist/generators/html-generator.d.ts.map +1 -0
- package/dist/generators/html-generator.js +772 -0
- package/dist/generators/html-generator.js.map +1 -0
- package/dist/generators/jquery-converter.d.ts +41 -0
- package/dist/generators/jquery-converter.d.ts.map +1 -0
- package/dist/generators/jquery-converter.js +594 -0
- package/dist/generators/jquery-converter.js.map +1 -0
- package/dist/generators/pattern-implementer.d.ts +26 -0
- package/dist/generators/pattern-implementer.d.ts.map +1 -0
- package/dist/generators/pattern-implementer.js +336 -0
- package/dist/generators/pattern-implementer.js.map +1 -0
- package/dist/generators/performance-generator.d.ts +51 -0
- package/dist/generators/performance-generator.d.ts.map +1 -0
- package/dist/generators/performance-generator.js +428 -0
- package/dist/generators/performance-generator.js.map +1 -0
- package/dist/generators/router-generator.d.ts +21 -0
- package/dist/generators/router-generator.d.ts.map +1 -0
- package/dist/generators/router-generator.js +178 -0
- package/dist/generators/router-generator.js.map +1 -0
- package/dist/generators/scaffolder.d.ts +28 -0
- package/dist/generators/scaffolder.d.ts.map +1 -0
- package/dist/generators/scaffolder.js +266 -0
- package/dist/generators/scaffolder.js.map +1 -0
- package/dist/generators/seo-generator.d.ts +29 -0
- package/dist/generators/seo-generator.d.ts.map +1 -0
- package/dist/generators/seo-generator.js +223 -0
- package/dist/generators/seo-generator.js.map +1 -0
- package/dist/generators/test-generator.d.ts +19 -0
- package/dist/generators/test-generator.d.ts.map +1 -0
- package/dist/generators/test-generator.js +398 -0
- package/dist/generators/test-generator.js.map +1 -0
- package/dist/generators/type-generator.d.ts +33 -0
- package/dist/generators/type-generator.d.ts.map +1 -0
- package/dist/generators/type-generator.js +663 -0
- package/dist/generators/type-generator.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/css-processor.d.ts +23 -0
- package/dist/parsers/css-processor.d.ts.map +1 -0
- package/dist/parsers/css-processor.js +129 -0
- package/dist/parsers/css-processor.js.map +1 -0
- package/dist/parsers/framework-parser.d.ts +48 -0
- package/dist/parsers/framework-parser.d.ts.map +1 -0
- package/dist/parsers/framework-parser.js +770 -0
- package/dist/parsers/framework-parser.js.map +1 -0
- package/dist/parsers/html-parser.d.ts +12 -0
- package/dist/parsers/html-parser.d.ts.map +1 -0
- package/dist/parsers/html-parser.js +444 -0
- package/dist/parsers/html-parser.js.map +1 -0
- package/dist/parsers/js-analyzer.d.ts +199 -0
- package/dist/parsers/js-analyzer.d.ts.map +1 -0
- package/dist/parsers/js-analyzer.js +680 -0
- package/dist/parsers/js-analyzer.js.map +1 -0
- package/dist/parsers/js-resolver.d.ts +8 -0
- package/dist/parsers/js-resolver.d.ts.map +1 -0
- package/dist/parsers/js-resolver.js +45 -0
- package/dist/parsers/js-resolver.js.map +1 -0
- package/dist/parsers/tailwind-detector.d.ts +23 -0
- package/dist/parsers/tailwind-detector.d.ts.map +1 -0
- package/dist/parsers/tailwind-detector.js +104 -0
- package/dist/parsers/tailwind-detector.js.map +1 -0
- package/dist/tests/advanced-features.test.d.ts +2 -0
- package/dist/tests/advanced-features.test.d.ts.map +1 -0
- package/dist/tests/advanced-features.test.js +235 -0
- package/dist/tests/advanced-features.test.js.map +1 -0
- package/dist/tests/css-modules.test.d.ts +2 -0
- package/dist/tests/css-modules.test.d.ts.map +1 -0
- package/dist/tests/css-modules.test.js +61 -0
- package/dist/tests/css-modules.test.js.map +1 -0
- package/dist/tests/css-processor.test.d.ts +2 -0
- package/dist/tests/css-processor.test.d.ts.map +1 -0
- package/dist/tests/css-processor.test.js +48 -0
- package/dist/tests/css-processor.test.js.map +1 -0
- package/dist/tests/html-parser.test.d.ts +2 -0
- package/dist/tests/html-parser.test.d.ts.map +1 -0
- package/dist/tests/html-parser.test.js +78 -0
- package/dist/tests/html-parser.test.js.map +1 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +65 -0
- package/dist/tests/integration.test.js.map +1 -0
- package/dist/tests/js-analyzer.test.d.ts +2 -0
- package/dist/tests/js-analyzer.test.d.ts.map +1 -0
- package/dist/tests/js-analyzer.test.js +58 -0
- package/dist/tests/js-analyzer.test.js.map +1 -0
- package/dist/tests/naming.test.d.ts +2 -0
- package/dist/tests/naming.test.d.ts.map +1 -0
- package/dist/tests/naming.test.js +43 -0
- package/dist/tests/naming.test.js.map +1 -0
- package/dist/tests/router-generator.test.d.ts +2 -0
- package/dist/tests/router-generator.test.d.ts.map +1 -0
- package/dist/tests/router-generator.test.js +60 -0
- package/dist/tests/router-generator.test.js.map +1 -0
- package/dist/tui/chat.d.ts +13 -0
- package/dist/tui/chat.d.ts.map +1 -0
- package/dist/tui/chat.js +499 -0
- package/dist/tui/chat.js.map +1 -0
- package/dist/tui/design-guide.d.ts +41 -0
- package/dist/tui/design-guide.d.ts.map +1 -0
- package/dist/tui/design-guide.js +184 -0
- package/dist/tui/design-guide.js.map +1 -0
- package/dist/tui/input.d.ts +30 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +239 -0
- package/dist/tui/input.js.map +1 -0
- package/dist/tui/renderer.d.ts +48 -0
- package/dist/tui/renderer.d.ts.map +1 -0
- package/dist/tui/renderer.js +212 -0
- package/dist/tui/renderer.js.map +1 -0
- package/dist/tui/tools.d.ts +14 -0
- package/dist/tui/tools.d.ts.map +1 -0
- package/dist/tui/tools.js +1370 -0
- package/dist/tui/tools.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config.d.ts +20 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +33 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/formatter.d.ts +5 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +68 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +19 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/naming.d.ts +17 -0
- package/dist/utils/naming.d.ts.map +1 -0
- package/dist/utils/naming.js +48 -0
- package/dist/utils/naming.js.map +1 -0
- package/dist/utils/report.d.ts +56 -0
- package/dist/utils/report.d.ts.map +1 -0
- package/dist/utils/report.js +339 -0
- package/dist/utils/report.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
import * as t from '@babel/types';
|
|
4
|
+
import _generate from '@babel/generator';
|
|
5
|
+
import { toValidIdentifier } from '../utils/naming.js';
|
|
6
|
+
const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
|
|
7
|
+
const generate = typeof _generate === 'function' ? _generate : _generate.default;
|
|
8
|
+
function emptyAnalysis() {
|
|
9
|
+
return {
|
|
10
|
+
stateVars: [], effects: [], eventHandlers: [], imports: [], utilityCode: [],
|
|
11
|
+
domManipulations: [], refs: [], fetchCalls: [], storageCalls: [],
|
|
12
|
+
jqueryPatterns: [], domMutations: [], timerCalls: [], pageTemplates: [],
|
|
13
|
+
interactivePatterns: { scrollReveals: [], tabs: [], accordions: [], scrollClasses: [], counterAnimations: [], carousels: [], eventDelegations: [], debounces: [], formValidations: [], historyRouting: [], mobileMenus: [], formHandlers: [], ripples: [] },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Detect common interactive patterns via regex (IntersectionObserver, tabs, accordion, scroll classes, counters).
|
|
18
|
+
* These patterns are too high-level for AST-based detection, so we use regex on the raw source.
|
|
19
|
+
*/
|
|
20
|
+
export function detectInteractivePatterns(code) {
|
|
21
|
+
const patterns = {
|
|
22
|
+
scrollReveals: [],
|
|
23
|
+
tabs: [],
|
|
24
|
+
accordions: [],
|
|
25
|
+
scrollClasses: [],
|
|
26
|
+
counterAnimations: [],
|
|
27
|
+
carousels: [],
|
|
28
|
+
eventDelegations: [],
|
|
29
|
+
debounces: [],
|
|
30
|
+
formValidations: [],
|
|
31
|
+
historyRouting: [],
|
|
32
|
+
mobileMenus: [],
|
|
33
|
+
formHandlers: [],
|
|
34
|
+
ripples: [],
|
|
35
|
+
};
|
|
36
|
+
// ── Scroll reveal: IntersectionObserver + animationPlayState = 'running' or classList.add ──
|
|
37
|
+
// Pattern: new IntersectionObserver(...) + querySelectorAll('.reveal')
|
|
38
|
+
const ioRevealRe = /new\s+IntersectionObserver\s*\(\s*(?:\([^)]*\)|[^,]*)\s*=>\s*\{[\s\S]*?animationPlayState\s*=\s*['"]running['"]/g;
|
|
39
|
+
if (ioRevealRe.test(code)) {
|
|
40
|
+
// Find the selector being observed
|
|
41
|
+
const selectorMatch = code.match(/querySelectorAll\s*\(\s*['"]([^'"]+)['"]\s*\)[\s\S]{0,200}?\.observe\b/);
|
|
42
|
+
const thresholdMatch = code.match(/threshold\s*:\s*([\d.]+)/);
|
|
43
|
+
patterns.scrollReveals.push({
|
|
44
|
+
selector: selectorMatch ? selectorMatch[1] : '.reveal',
|
|
45
|
+
property: 'animationPlayState',
|
|
46
|
+
activeValue: 'running',
|
|
47
|
+
threshold: thresholdMatch ? parseFloat(thresholdMatch[1]) : 0.1,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Also detect classList-based reveal: IntersectionObserver + classList.add('visible'/'active'/'show')
|
|
51
|
+
const ioClassRevealRe = /new\s+IntersectionObserver\s*\([\s\S]*?classList\.add\s*\(\s*['"](visible|active|show|revealed|in-view)['"]\s*\)/;
|
|
52
|
+
const classRevealMatch = code.match(ioClassRevealRe);
|
|
53
|
+
if (classRevealMatch && patterns.scrollReveals.length === 0) {
|
|
54
|
+
const selectorMatch = code.match(/querySelectorAll\s*\(\s*['"]([^'"]+)['"]\s*\)[\s\S]{0,200}?\.observe\b/);
|
|
55
|
+
patterns.scrollReveals.push({
|
|
56
|
+
selector: selectorMatch ? selectorMatch[1] : '.reveal',
|
|
57
|
+
property: 'classList',
|
|
58
|
+
activeValue: classRevealMatch[1],
|
|
59
|
+
threshold: 0.1,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ── Tab pattern: querySelectorAll('.tab').forEach + classList.add/remove('active') + data-tab ──
|
|
63
|
+
const tabRe = /querySelectorAll\s*\(\s*['"]([^'"]*tab[^'"]*)['"]\s*\)[\s\S]*?\.addEventListener\s*\(\s*['"]click['"]/i;
|
|
64
|
+
const tabMatch = code.match(tabRe);
|
|
65
|
+
if (tabMatch) {
|
|
66
|
+
const tabSelector = tabMatch[1];
|
|
67
|
+
// Try to find the panel selector and data attribute
|
|
68
|
+
const dataAttrMatch = code.match(/(?:tab|btn)\s*\.dataset\.(\w+)/i) || code.match(/\.dataset\.(\w+)/);
|
|
69
|
+
const panelMatch = code.match(/querySelectorAll\s*\(\s*['"]([^'"]*panel[^'"]*)['"]\s*\)/i);
|
|
70
|
+
const activeMatch = code.match(/classList\.\w+\s*\(\s*['"](active|selected|current)['"]\s*\)/);
|
|
71
|
+
patterns.tabs.push({
|
|
72
|
+
tabSelector,
|
|
73
|
+
panelSelector: panelMatch ? panelMatch[1] : tabSelector.replace('tab', 'panel'),
|
|
74
|
+
dataAttr: dataAttrMatch ? `data-${dataAttrMatch[1]}` : 'data-tab',
|
|
75
|
+
activeClass: activeMatch ? activeMatch[1] : 'active',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ── Accordion/FAQ pattern: querySelectorAll('.faq-q'/.accordion-trigger) + classList.toggle/add('open'/'active') ──
|
|
79
|
+
const accordionRe = /querySelectorAll\s*\(\s*['"]([^'"]*(?:faq|accordion|collapse|toggle)[^'"]*)['"]\s*\)[\s\S]*?\.addEventListener\s*\(\s*['"]click['"]/i;
|
|
80
|
+
const accordionMatch = code.match(accordionRe);
|
|
81
|
+
if (accordionMatch) {
|
|
82
|
+
const triggerSelector = accordionMatch[1];
|
|
83
|
+
const openClassMatch = code.match(/classList\.(?:toggle|add|contains)\s*\(\s*['"](open|active|expanded|show)['"]\s*\)/);
|
|
84
|
+
const itemMatch = code.match(/closest\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
85
|
+
// Check if single-open: look for forEach(... .remove('open')) pattern
|
|
86
|
+
const singleOpen = /querySelectorAll[\s\S]*?forEach[\s\S]*?classList\.remove\s*\(\s*['"](open|active|expanded|show)['"]\s*\)/.test(code);
|
|
87
|
+
patterns.accordions.push({
|
|
88
|
+
triggerSelector,
|
|
89
|
+
itemSelector: itemMatch ? itemMatch[1] : triggerSelector.replace(/-q$|-trigger$|-btn$/, '-item'),
|
|
90
|
+
openClass: openClassMatch ? openClassMatch[1] : 'open',
|
|
91
|
+
singleOpen,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// ── Scroll class toggle: window scroll + classList.toggle('scrolled', scrollY > N) ──
|
|
95
|
+
const scrollClassRe = /(?:window|document)\.addEventListener\s*\(\s*['"]scroll['"][\s\S]*?(?:getElementById\s*\(\s*['"](\w+)['"]\s*\)|querySelector\s*\(\s*['"]([^'"]+)['"]\s*\))[\s\S]*?classList\.toggle\s*\(\s*['"](\w+)['"]/;
|
|
96
|
+
const scrollClassMatch = code.match(scrollClassRe);
|
|
97
|
+
if (scrollClassMatch) {
|
|
98
|
+
const thresholdMatch = code.match(/scrollY\s*>\s*(\d+)/);
|
|
99
|
+
patterns.scrollClasses.push({
|
|
100
|
+
elementSelector: scrollClassMatch[1] ? `#${scrollClassMatch[1]}` : scrollClassMatch[2],
|
|
101
|
+
className: scrollClassMatch[3],
|
|
102
|
+
threshold: thresholdMatch ? parseInt(thresholdMatch[1], 10) : 0,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// ── Counter animation: IntersectionObserver + data-target + textContent ──
|
|
106
|
+
const counterRe = /new\s+IntersectionObserver[\s\S]*?dataset\.target[\s\S]*?textContent/;
|
|
107
|
+
if (counterRe.test(code)) {
|
|
108
|
+
const counterSelectorMatch = code.match(/querySelectorAll\s*\(\s*['"]([^'"]*(?:data-target|stat-num|counter)[^'"]*)['"]\s*\)/);
|
|
109
|
+
patterns.counterAnimations.push({
|
|
110
|
+
selector: counterSelectorMatch ? counterSelectorMatch[1] : '[data-target]',
|
|
111
|
+
dataAttr: 'data-target',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// ── Carousel/slider: currentSlide/slideIndex + translateX/transform ──
|
|
115
|
+
const carouselStateRe = /(?:let|var)\s+(currentSlide|slideIndex|currentIndex|activeSlide)\s*=\s*(\d+)/;
|
|
116
|
+
const carouselStateMatch = code.match(carouselStateRe);
|
|
117
|
+
if (carouselStateMatch) {
|
|
118
|
+
const containerMatch = code.match(/querySelector\s*\(\s*['"]([^'"]*(?:slide|carousel|swiper|gallery)[^'"]*)['"]\s*\)/i);
|
|
119
|
+
const autoPlayMatch = /setInterval[\s\S]*?(?:slide|next|advance)/i.test(code);
|
|
120
|
+
patterns.carousels.push({
|
|
121
|
+
containerSelector: containerMatch ? containerMatch[1] : '.carousel',
|
|
122
|
+
stateVar: carouselStateMatch[1],
|
|
123
|
+
autoPlay: autoPlayMatch,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// ── Event delegation: parent.addEventListener('click', e => { if (e.target.matches/classList.contains) ... }) ──
|
|
127
|
+
const delegationRe = /(?:querySelector|getElementById)\s*\(\s*['"]([^'"]+)['"]\s*\)[\s\S]*?addEventListener\s*\(\s*['"](\w+)['"][\s\S]*?(?:e\.target|event\.target)[\s\S]*?(?:\.matches\s*\(\s*['"]([^'"]+)['"]|\.classList\.contains\s*\(\s*['"]([^'"]+)['"])/g;
|
|
128
|
+
let delegationMatch;
|
|
129
|
+
while ((delegationMatch = delegationRe.exec(code)) !== null) {
|
|
130
|
+
patterns.eventDelegations.push({
|
|
131
|
+
parentSelector: delegationMatch[1],
|
|
132
|
+
targetMatch: delegationMatch[3] || delegationMatch[4],
|
|
133
|
+
event: delegationMatch[2],
|
|
134
|
+
handler: '',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// ── Debounce/throttle: debounce(fn, delay) or setTimeout wrapper pattern ──
|
|
138
|
+
const debounceRe = /(?:debounce|throttle)\s*\(\s*(\w+)\s*,\s*(\d+)/g;
|
|
139
|
+
let debounceMatch;
|
|
140
|
+
while ((debounceMatch = debounceRe.exec(code)) !== null) {
|
|
141
|
+
patterns.debounces.push({
|
|
142
|
+
functionName: debounceMatch[1],
|
|
143
|
+
delay: debounceMatch[2],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Also detect inline debounce pattern: clearTimeout + setTimeout
|
|
147
|
+
const inlineDebounceRe = /clearTimeout\s*\(\s*(\w+)\s*\)[\s\S]{0,50}?(\w+)\s*=\s*setTimeout/;
|
|
148
|
+
if (inlineDebounceRe.test(code) && patterns.debounces.length === 0) {
|
|
149
|
+
patterns.debounces.push({ functionName: 'inlineDebounce', delay: '300' });
|
|
150
|
+
}
|
|
151
|
+
// ── Form validation: checkValidity, setCustomValidity, reportValidity ──
|
|
152
|
+
const validationRe = /(?:checkValidity|setCustomValidity|reportValidity)\s*\(/;
|
|
153
|
+
if (validationRe.test(code)) {
|
|
154
|
+
const formMatch = code.match(/(?:querySelector|getElementById)\s*\(\s*['"]([^'"]*form[^'"]*)['"]\s*\)/i);
|
|
155
|
+
const methods = [];
|
|
156
|
+
if (/checkValidity/.test(code))
|
|
157
|
+
methods.push('checkValidity');
|
|
158
|
+
if (/setCustomValidity/.test(code))
|
|
159
|
+
methods.push('setCustomValidity');
|
|
160
|
+
if (/reportValidity/.test(code))
|
|
161
|
+
methods.push('reportValidity');
|
|
162
|
+
patterns.formValidations.push({
|
|
163
|
+
formSelector: formMatch ? formMatch[1] : 'form',
|
|
164
|
+
methods,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// ── History/hash routing: history.pushState or hashchange ──
|
|
168
|
+
const pushStateRe = /history\.pushState|history\.replaceState|\.pushState\s*\(/;
|
|
169
|
+
const hashChangeRe = /hashchange|window\.location\.hash/;
|
|
170
|
+
if (pushStateRe.test(code)) {
|
|
171
|
+
const routeMatches = code.match(/pushState\s*\([^,]*,\s*[^,]*,\s*['"]([^'"]+)['"]\s*\)/g) || [];
|
|
172
|
+
const routes = routeMatches.map(m => {
|
|
173
|
+
const match = m.match(/['"]([^'"]+)['"]\s*\)$/);
|
|
174
|
+
return match ? match[1] : '/';
|
|
175
|
+
});
|
|
176
|
+
patterns.historyRouting.push({ routes, type: 'pushState' });
|
|
177
|
+
}
|
|
178
|
+
else if (hashChangeRe.test(code)) {
|
|
179
|
+
const hashMatches = code.match(/(?:location\.hash\s*===?\s*['"]#?([^'"]+)['"]|case\s+['"]#?([^'"]+)['"])/g) || [];
|
|
180
|
+
const routes = hashMatches.map(m => {
|
|
181
|
+
const match = m.match(/['"]#?([^'"]+)['"]/);
|
|
182
|
+
return match ? `/${match[1]}` : '/';
|
|
183
|
+
});
|
|
184
|
+
patterns.historyRouting.push({ routes, type: 'hash' });
|
|
185
|
+
}
|
|
186
|
+
// ── Mobile menu: hamburger button toggle + mobile nav open/close ──
|
|
187
|
+
const mobileMenuRe = /(?:getElementById|querySelector)\s*\(\s*['"]([^'"]*(?:ham|hamburger|burger|menu-toggle|nav-toggle|mobile-btn)[^'"]*)['"]\s*\)[\s\S]*?addEventListener\s*\(\s*['"]click['"]/i;
|
|
188
|
+
const mobileMenuMatch = code.match(mobileMenuRe);
|
|
189
|
+
if (mobileMenuMatch) {
|
|
190
|
+
const mobMatch = code.match(/(?:getElementById|querySelector)\s*\(\s*['"]([^'"]*(?:mob|mobile-menu|mobile-nav|nav-mobile|side-menu|offcanvas)[^'"]*)['"]\s*\)/i);
|
|
191
|
+
const openClassMatch = code.match(/classList\.toggle\s*\(\s*['"](open|active|show|visible)['"]\s*\)/);
|
|
192
|
+
const bodyOverflow = /document\.body\.style\.overflow/.test(code);
|
|
193
|
+
patterns.mobileMenus.push({
|
|
194
|
+
hamburgerSelector: mobileMenuMatch[1].startsWith('.') || mobileMenuMatch[1].startsWith('#') ? mobileMenuMatch[1] : `#${mobileMenuMatch[1]}`,
|
|
195
|
+
menuSelector: mobMatch ? (mobMatch[1].startsWith('.') || mobMatch[1].startsWith('#') ? mobMatch[1] : `#${mobMatch[1]}`) : '.mobile-menu',
|
|
196
|
+
openClass: openClassMatch ? openClassMatch[1] : 'open',
|
|
197
|
+
togglesBodyOverflow: bodyOverflow,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// ── Form handlers: email validation + success feedback ──
|
|
201
|
+
const formHandlerRe = /function\s+(\w*(?:handle|submit|cta)\w*)\s*\([^)]*\)\s*\{[\s\S]*?(?:includes\s*\(\s*['"]@['"]\s*\)|\.value[\s\S]*?@)/i;
|
|
202
|
+
const formHandlerMatch = code.match(formHandlerRe);
|
|
203
|
+
if (formHandlerMatch) {
|
|
204
|
+
const inputMatch = code.match(/(?:getElementById|querySelector)\s*\(\s*['"]([^'"]*(?:email|input|cta)[^'"]*)['"]\s*\)/i);
|
|
205
|
+
const hasSuccessFeedback = /textContent\s*=\s*['"][^'"]*(?:list|success|thank|sent)/i.test(code) ||
|
|
206
|
+
/style\.background\s*=\s*['"]#3ecf6e['"]/i.test(code);
|
|
207
|
+
patterns.formHandlers.push({
|
|
208
|
+
inputSelector: inputMatch ? inputMatch[1] : '#ctaEmail',
|
|
209
|
+
buttonSelector: '.input-group button',
|
|
210
|
+
handlerName: formHandlerMatch[1],
|
|
211
|
+
isEmail: true,
|
|
212
|
+
hasSuccessFeedback,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// ── Ripple effect: createElement('span') + animation:ripple + getBoundingClientRect ──
|
|
216
|
+
const rippleRe = /createElement\s*\(\s*['"]span['"]\s*\)[\s\S]*?(?:ripple|getBoundingClientRect)/;
|
|
217
|
+
if (rippleRe.test(code)) {
|
|
218
|
+
const selectorMatch = code.match(/querySelectorAll\s*\(\s*['"]([^'"]+)['"]\s*\)[\s\S]{0,200}?(?:ripple|createElement)/);
|
|
219
|
+
if (selectorMatch) {
|
|
220
|
+
const selectors = selectorMatch[1].split(',').map(s => s.trim());
|
|
221
|
+
patterns.ripples.push({ selectors });
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
patterns.ripples.push({ selectors: ['.btn-primary', '.plan-btn'] });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return patterns;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Analyze vanilla JavaScript using Babel AST and extract React-convertible patterns.
|
|
231
|
+
*/
|
|
232
|
+
export function analyzeJS(code) {
|
|
233
|
+
const result = emptyAnalysis();
|
|
234
|
+
let ast;
|
|
235
|
+
try {
|
|
236
|
+
ast = parse(code, {
|
|
237
|
+
sourceType: 'unambiguous',
|
|
238
|
+
allowReturnOutsideFunction: true,
|
|
239
|
+
plugins: ['optionalChaining', 'nullishCoalescingOperator'],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return fallbackAnalyze(code);
|
|
244
|
+
}
|
|
245
|
+
// Track which variables are mutated (for useState detection)
|
|
246
|
+
const declaredVars = new Map();
|
|
247
|
+
// Pass 1: collect variable declarations
|
|
248
|
+
traverse(ast, {
|
|
249
|
+
VariableDeclaration(path) {
|
|
250
|
+
if (path.node.kind === 'const')
|
|
251
|
+
return;
|
|
252
|
+
for (const decl of path.node.declarations) {
|
|
253
|
+
if (!t.isIdentifier(decl.id))
|
|
254
|
+
continue;
|
|
255
|
+
const name = decl.id.name;
|
|
256
|
+
const init = decl.init ? generate(decl.init).code : 'undefined';
|
|
257
|
+
if (decl.init && isDOMCallExpression(decl.init))
|
|
258
|
+
continue;
|
|
259
|
+
declaredVars.set(name, { init, mutated: false });
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
// Pass 2: detect mutations
|
|
264
|
+
traverse(ast, {
|
|
265
|
+
AssignmentExpression(path) {
|
|
266
|
+
if (t.isIdentifier(path.node.left)) {
|
|
267
|
+
const entry = declaredVars.get(path.node.left.name);
|
|
268
|
+
if (entry)
|
|
269
|
+
entry.mutated = true;
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
UpdateExpression(path) {
|
|
273
|
+
if (t.isIdentifier(path.node.argument)) {
|
|
274
|
+
const entry = declaredVars.get(path.node.argument.name);
|
|
275
|
+
if (entry)
|
|
276
|
+
entry.mutated = true;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
for (const [name, info] of declaredVars) {
|
|
281
|
+
if (info.mutated) {
|
|
282
|
+
const safeName = toValidIdentifier(name);
|
|
283
|
+
result.stateVars.push({
|
|
284
|
+
name: safeName,
|
|
285
|
+
initialValue: convertInitialValue(info.init),
|
|
286
|
+
setter: `set${safeName.charAt(0).toUpperCase()}${safeName.slice(1)}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Pass 3: event listeners, DOM manipulations, DOMContentLoaded, fetch, storage, timers, jQuery
|
|
291
|
+
traverse(ast, {
|
|
292
|
+
CallExpression(path) {
|
|
293
|
+
const { node } = path;
|
|
294
|
+
// --- addEventListener ---
|
|
295
|
+
if (t.isMemberExpression(node.callee) &&
|
|
296
|
+
t.isIdentifier(node.callee.property, { name: 'addEventListener' }) &&
|
|
297
|
+
node.arguments.length >= 2) {
|
|
298
|
+
const eventArg = node.arguments[0];
|
|
299
|
+
const handlerArg = node.arguments[1];
|
|
300
|
+
if (t.isStringLiteral(eventArg)) {
|
|
301
|
+
const eventName = eventArg.value;
|
|
302
|
+
if (eventName === 'DOMContentLoaded' || eventName === 'load') {
|
|
303
|
+
const handlerBody = extractFunctionBody(handlerArg);
|
|
304
|
+
if (handlerBody)
|
|
305
|
+
result.effects.push(handlerBody);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const selector = extractDOMSelector(node.callee.object);
|
|
309
|
+
if (selector) {
|
|
310
|
+
result.eventHandlers.push({
|
|
311
|
+
elementSelector: selector,
|
|
312
|
+
event: eventName,
|
|
313
|
+
handler: extractFunctionBody(handlerArg) || (t.isIdentifier(handlerArg) ? handlerArg.name : ''),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// --- DOM query calls → refs ---
|
|
319
|
+
if (t.isMemberExpression(node.callee)) {
|
|
320
|
+
const method = t.isIdentifier(node.callee.property) ? node.callee.property.name : '';
|
|
321
|
+
if (['getElementById', 'querySelector', 'getElementsByClassName', 'querySelectorAll'].includes(method)) {
|
|
322
|
+
const selectorArg = node.arguments[0];
|
|
323
|
+
if (t.isStringLiteral(selectorArg)) {
|
|
324
|
+
result.domManipulations.push({
|
|
325
|
+
type: method,
|
|
326
|
+
selector: selectorArg.value,
|
|
327
|
+
action: '', value: '',
|
|
328
|
+
});
|
|
329
|
+
if (method === 'getElementById') {
|
|
330
|
+
const refName = toValidIdentifier(selectorArg.value) + 'Ref';
|
|
331
|
+
if (!result.refs.find(r => r.name === refName)) {
|
|
332
|
+
result.refs.push({ name: refName, selector: selectorArg.value, type: 'HTMLElement' });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// --- fetch() calls ---
|
|
339
|
+
if (t.isIdentifier(node.callee, { name: 'fetch' }) && node.arguments.length >= 1) {
|
|
340
|
+
const urlArg = node.arguments[0];
|
|
341
|
+
const url = t.isStringLiteral(urlArg) ? urlArg.value : generate(urlArg).code;
|
|
342
|
+
let method = 'GET';
|
|
343
|
+
let hasBody = false;
|
|
344
|
+
if (node.arguments[1] && t.isObjectExpression(node.arguments[1])) {
|
|
345
|
+
for (const prop of node.arguments[1].properties) {
|
|
346
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
|
347
|
+
if (prop.key.name === 'method' && t.isStringLiteral(prop.value)) {
|
|
348
|
+
method = prop.value.value;
|
|
349
|
+
}
|
|
350
|
+
if (prop.key.name === 'body')
|
|
351
|
+
hasBody = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
result.fetchCalls.push({ url, method, hasBody, variableName: '' });
|
|
356
|
+
}
|
|
357
|
+
// --- localStorage / sessionStorage ---
|
|
358
|
+
if (t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object)) {
|
|
359
|
+
const objName = node.callee.object.name;
|
|
360
|
+
if ((objName === 'localStorage' || objName === 'sessionStorage') && t.isIdentifier(node.callee.property)) {
|
|
361
|
+
const op = node.callee.property.name;
|
|
362
|
+
if (['getItem', 'setItem', 'removeItem'].includes(op)) {
|
|
363
|
+
const keyArg = node.arguments[0];
|
|
364
|
+
const key = t.isStringLiteral(keyArg) ? keyArg.value : generate(keyArg).code;
|
|
365
|
+
const value = node.arguments[1] ? generate(node.arguments[1]).code : undefined;
|
|
366
|
+
result.storageCalls.push({
|
|
367
|
+
storageType: objName,
|
|
368
|
+
operation: op,
|
|
369
|
+
key,
|
|
370
|
+
value,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// --- setTimeout / setInterval ---
|
|
376
|
+
if (t.isIdentifier(node.callee) && ['setTimeout', 'setInterval'].includes(node.callee.name)) {
|
|
377
|
+
const handler = extractFunctionBody(node.arguments[0]) || '';
|
|
378
|
+
const delay = node.arguments[1] ? generate(node.arguments[1]).code : '0';
|
|
379
|
+
result.timerCalls.push({
|
|
380
|
+
type: node.callee.name,
|
|
381
|
+
handler,
|
|
382
|
+
delay,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// --- jQuery patterns ---
|
|
386
|
+
if (t.isIdentifier(node.callee, { name: '$' }) || t.isIdentifier(node.callee, { name: 'jQuery' })) {
|
|
387
|
+
if (node.arguments.length >= 1 && t.isStringLiteral(node.arguments[0])) {
|
|
388
|
+
const selector = node.arguments[0].value;
|
|
389
|
+
// Walk up to find chained method calls
|
|
390
|
+
const parent = path.parent;
|
|
391
|
+
if (t.isMemberExpression(parent) && t.isIdentifier(parent.property)) {
|
|
392
|
+
const grandParent = path.parentPath?.parent;
|
|
393
|
+
if (t.isCallExpression(grandParent)) {
|
|
394
|
+
const args = grandParent.arguments.map((a) => generate(a).code).join(', ');
|
|
395
|
+
result.jqueryPatterns.push({
|
|
396
|
+
selector,
|
|
397
|
+
method: parent.property.name,
|
|
398
|
+
args,
|
|
399
|
+
original: generate(grandParent).code,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// --- $.ajax / $.get / $.post ---
|
|
406
|
+
if (t.isMemberExpression(node.callee) &&
|
|
407
|
+
t.isIdentifier(node.callee.object, { name: '$' }) &&
|
|
408
|
+
t.isIdentifier(node.callee.property)) {
|
|
409
|
+
const ajaxMethod = node.callee.property.name;
|
|
410
|
+
if (['ajax', 'get', 'post', 'getJSON'].includes(ajaxMethod)) {
|
|
411
|
+
const urlArg = node.arguments[0];
|
|
412
|
+
const url = t.isStringLiteral(urlArg) ? urlArg.value : generate(urlArg).code;
|
|
413
|
+
const method = ajaxMethod === 'post' ? 'POST' : 'GET';
|
|
414
|
+
result.fetchCalls.push({ url, method, hasBody: ajaxMethod === 'post', variableName: '' });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
// Pass 3b: detect property assignments on DOM elements (e.g., element.onclick = ...)
|
|
420
|
+
// and DOM mutations (innerHTML, textContent, classList, style)
|
|
421
|
+
traverse(ast, {
|
|
422
|
+
AssignmentExpression(path) {
|
|
423
|
+
const { node } = path;
|
|
424
|
+
if (!t.isMemberExpression(node.left))
|
|
425
|
+
return;
|
|
426
|
+
const prop = t.isIdentifier(node.left.property) ? node.left.property.name : '';
|
|
427
|
+
// onclick-style assignments
|
|
428
|
+
if (prop.startsWith('on')) {
|
|
429
|
+
const selector = extractDOMSelector(node.left.object);
|
|
430
|
+
if (selector) {
|
|
431
|
+
const eventName = prop.replace(/^on/, '');
|
|
432
|
+
result.eventHandlers.push({
|
|
433
|
+
elementSelector: selector,
|
|
434
|
+
event: eventName,
|
|
435
|
+
handler: extractFunctionBody(node.right) || generate(node.right).code,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// DOM mutations: innerHTML, textContent
|
|
440
|
+
if (prop === 'innerHTML' || prop === 'textContent') {
|
|
441
|
+
const selector = extractDOMSelectorDeep(node.left.object);
|
|
442
|
+
if (selector) {
|
|
443
|
+
result.domMutations.push({
|
|
444
|
+
type: prop,
|
|
445
|
+
selector,
|
|
446
|
+
value: generate(node.right).code,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Style mutations: element.style.X = Y
|
|
451
|
+
if (t.isMemberExpression(node.left.object) && t.isIdentifier(node.left.object.property, { name: 'style' })) {
|
|
452
|
+
const selector = extractDOMSelectorDeep(node.left.object.object);
|
|
453
|
+
if (selector) {
|
|
454
|
+
result.domMutations.push({
|
|
455
|
+
type: 'style',
|
|
456
|
+
selector,
|
|
457
|
+
value: `${prop}: ${generate(node.right).code}`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
// Pass 3c: detect classList mutations
|
|
464
|
+
traverse(ast, {
|
|
465
|
+
CallExpression(path) {
|
|
466
|
+
const { node } = path;
|
|
467
|
+
if (t.isMemberExpression(node.callee) && t.isMemberExpression(node.callee.object)) {
|
|
468
|
+
const innerProp = t.isIdentifier(node.callee.object.property) ? node.callee.object.property.name : '';
|
|
469
|
+
const method = t.isIdentifier(node.callee.property) ? node.callee.property.name : '';
|
|
470
|
+
if (innerProp === 'classList' && ['add', 'remove', 'toggle', 'replace'].includes(method)) {
|
|
471
|
+
const selector = extractDOMSelectorDeep(node.callee.object.object);
|
|
472
|
+
if (selector) {
|
|
473
|
+
const className = node.arguments[0] ? generate(node.arguments[0]).code : '';
|
|
474
|
+
result.domMutations.push({
|
|
475
|
+
type: `classList.${method}`,
|
|
476
|
+
selector,
|
|
477
|
+
value: className,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
// Pass 3d: extract page templates (functions that set innerHTML with large template literals)
|
|
485
|
+
traverse(ast, {
|
|
486
|
+
FunctionDeclaration(path) {
|
|
487
|
+
const funcName = path.node.id?.name || '';
|
|
488
|
+
if (!funcName.match(/^render/i))
|
|
489
|
+
return;
|
|
490
|
+
// Look for innerHTML assignments with template literals inside this function
|
|
491
|
+
path.traverse({
|
|
492
|
+
AssignmentExpression(innerPath) {
|
|
493
|
+
const { node } = innerPath;
|
|
494
|
+
if (!t.isMemberExpression(node.left))
|
|
495
|
+
return;
|
|
496
|
+
const prop = t.isIdentifier(node.left.property) ? node.left.property.name : '';
|
|
497
|
+
if (prop !== 'innerHTML')
|
|
498
|
+
return;
|
|
499
|
+
// Check if the right side is a template literal with significant HTML content
|
|
500
|
+
if (t.isTemplateLiteral(node.right)) {
|
|
501
|
+
const rawHtml = node.right.quasis.map((q) => q.value.raw).join('');
|
|
502
|
+
// Only extract templates with substantial HTML content (> 100 chars with HTML tags)
|
|
503
|
+
if (rawHtml.length > 100 && /<[a-z]/.test(rawHtml)) {
|
|
504
|
+
const targetEl = extractDOMSelectorDeep(node.left.object);
|
|
505
|
+
// Infer route from function name (e.g., renderHomePage -> /, renderAboutPage -> /about)
|
|
506
|
+
const routeMatch = funcName.match(/^render(\w+?)Page$/i);
|
|
507
|
+
const route = routeMatch
|
|
508
|
+
? (routeMatch[1].toLowerCase() === 'home' ? '/' : `/${routeMatch[1].toLowerCase()}`)
|
|
509
|
+
: undefined;
|
|
510
|
+
result.pageTemplates.push({
|
|
511
|
+
functionName: funcName,
|
|
512
|
+
targetElement: targetEl || 'app',
|
|
513
|
+
htmlContent: rawHtml,
|
|
514
|
+
route,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
// Pass 4: extract utility functions
|
|
523
|
+
traverse(ast, {
|
|
524
|
+
FunctionDeclaration(path) {
|
|
525
|
+
const body = generate(path.node).code;
|
|
526
|
+
if (!isDOMRelatedCode(body)) {
|
|
527
|
+
result.utilityCode.push(body);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
// Pass 5: detect interactive patterns (scroll-reveal, tabs, accordion, scroll class, counters)
|
|
532
|
+
result.interactivePatterns = detectInteractivePatterns(code);
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
// --- Helpers ---
|
|
536
|
+
function extractDOMSelector(node) {
|
|
537
|
+
if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
|
|
538
|
+
const method = t.isIdentifier(node.callee.property) ? node.callee.property.name : '';
|
|
539
|
+
if (['getElementById', 'querySelector', 'getElementsByClassName'].includes(method)) {
|
|
540
|
+
const arg = node.arguments[0];
|
|
541
|
+
if (t.isStringLiteral(arg))
|
|
542
|
+
return arg.value;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
/** Walk deeper through variable references to find DOM selector. */
|
|
548
|
+
function extractDOMSelectorDeep(node) {
|
|
549
|
+
const direct = extractDOMSelector(node);
|
|
550
|
+
if (direct)
|
|
551
|
+
return direct;
|
|
552
|
+
// If it's an identifier, it might be a variable holding a DOM ref
|
|
553
|
+
if (t.isIdentifier(node))
|
|
554
|
+
return node.name;
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
function extractFunctionBody(node) {
|
|
558
|
+
if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) {
|
|
559
|
+
if (t.isBlockStatement(node.body)) {
|
|
560
|
+
const stmts = node.body.body.map((s) => generate(s).code);
|
|
561
|
+
return stmts.join('\n');
|
|
562
|
+
}
|
|
563
|
+
return generate(node.body).code;
|
|
564
|
+
}
|
|
565
|
+
if (t.isIdentifier(node))
|
|
566
|
+
return node.name;
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
function isDOMCallExpression(node) {
|
|
570
|
+
if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
|
|
571
|
+
const obj = node.callee.object;
|
|
572
|
+
const prop = node.callee.property;
|
|
573
|
+
if (t.isIdentifier(obj, { name: 'document' }) && t.isIdentifier(prop)) {
|
|
574
|
+
return ['getElementById', 'querySelector', 'querySelectorAll',
|
|
575
|
+
'getElementsByClassName', 'getElementsByTagName', 'createElement'].includes(prop.name);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
function isDOMRelatedCode(code) {
|
|
581
|
+
return /document\.|\.innerHTML|\.textContent|\.style\.|\.classList|\.addEventListener|\.appendChild|\.removeChild|\$\(/.test(code);
|
|
582
|
+
}
|
|
583
|
+
function convertInitialValue(value) {
|
|
584
|
+
if (value === 'true' || value === 'false')
|
|
585
|
+
return value;
|
|
586
|
+
if (value === 'null' || value === 'undefined')
|
|
587
|
+
return value;
|
|
588
|
+
if (/^\d+(\.\d+)?$/.test(value))
|
|
589
|
+
return value;
|
|
590
|
+
if (/^['"`]/.test(value))
|
|
591
|
+
return value;
|
|
592
|
+
if (value.startsWith('['))
|
|
593
|
+
return value;
|
|
594
|
+
if (value.startsWith('{'))
|
|
595
|
+
return value;
|
|
596
|
+
return `'${value}'`;
|
|
597
|
+
}
|
|
598
|
+
function fallbackAnalyze(code) {
|
|
599
|
+
const result = emptyAnalysis();
|
|
600
|
+
const varPattern = /(?:let|var)\s+(\w+)\s*=\s*([^;]+);/g;
|
|
601
|
+
let match;
|
|
602
|
+
while ((match = varPattern.exec(code)) !== null) {
|
|
603
|
+
const name = toValidIdentifier(match[1]);
|
|
604
|
+
const value = match[2].trim();
|
|
605
|
+
if (/document\./.test(value))
|
|
606
|
+
continue;
|
|
607
|
+
const mutationPattern = new RegExp(`\\b${match[1]}\\s*=(?!=)`, 'g');
|
|
608
|
+
const mutations = code.match(mutationPattern);
|
|
609
|
+
if (mutations && mutations.length > 1) {
|
|
610
|
+
result.stateVars.push({
|
|
611
|
+
name,
|
|
612
|
+
initialValue: convertInitialValue(value),
|
|
613
|
+
setter: `set${name.charAt(0).toUpperCase()}${name.slice(1)}`,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// Event listeners
|
|
618
|
+
const listenerPattern = /(?:document\.)?(?:getElementById|querySelector(?:All)?)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)\.addEventListener\s*\(\s*['"`](\w+)['"`]\s*,\s*(?:function\s*\([^)]*\)\s*\{([\s\S]*?)\}|(\w+))\s*\)/g;
|
|
619
|
+
let listenerMatch;
|
|
620
|
+
while ((listenerMatch = listenerPattern.exec(code)) !== null) {
|
|
621
|
+
result.eventHandlers.push({
|
|
622
|
+
elementSelector: listenerMatch[1],
|
|
623
|
+
event: listenerMatch[2],
|
|
624
|
+
handler: listenerMatch[3] || listenerMatch[4] || '',
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// DOMContentLoaded
|
|
628
|
+
const readyPattern = /document\.addEventListener\s*\(\s*['"`]DOMContentLoaded['"`]\s*,\s*(?:function\s*\([^)]*\)\s*\{([\s\S]*?)\}|\(\)\s*=>\s*\{([\s\S]*?)\})/g;
|
|
629
|
+
let readyMatch;
|
|
630
|
+
while ((readyMatch = readyPattern.exec(code)) !== null) {
|
|
631
|
+
const body = readyMatch[1] || readyMatch[2] || '';
|
|
632
|
+
if (body.trim())
|
|
633
|
+
result.effects.push(body.trim());
|
|
634
|
+
}
|
|
635
|
+
// fetch calls via regex
|
|
636
|
+
const fetchPattern = /fetch\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
637
|
+
let fetchMatch;
|
|
638
|
+
while ((fetchMatch = fetchPattern.exec(code)) !== null) {
|
|
639
|
+
result.fetchCalls.push({ url: fetchMatch[1], method: 'GET', hasBody: false, variableName: '' });
|
|
640
|
+
}
|
|
641
|
+
// localStorage/sessionStorage
|
|
642
|
+
const storagePattern = /(localStorage|sessionStorage)\.(getItem|setItem|removeItem)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
643
|
+
let storageMatch;
|
|
644
|
+
while ((storageMatch = storagePattern.exec(code)) !== null) {
|
|
645
|
+
result.storageCalls.push({
|
|
646
|
+
storageType: storageMatch[1],
|
|
647
|
+
operation: storageMatch[2],
|
|
648
|
+
key: storageMatch[3],
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
// Detect interactive patterns in fallback mode too
|
|
652
|
+
result.interactivePatterns = detectInteractivePatterns(code);
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Convert an event handler body from DOM-manipulating code to React-style code.
|
|
657
|
+
* Handles common patterns: state mutations, DOM reads, form resets, preventDefault.
|
|
658
|
+
*/
|
|
659
|
+
export function convertEventHandlerToReact(handler, stateVars) {
|
|
660
|
+
let converted = handler;
|
|
661
|
+
// Convert state variable mutations to setter calls
|
|
662
|
+
for (const sv of stateVars) {
|
|
663
|
+
converted = converted.replace(new RegExp(`${sv.name}\\s*=\\s*([^;]+)`, 'g'), `${sv.setter}($1)`);
|
|
664
|
+
converted = converted.replace(new RegExp(`${sv.name}\\s*\\+=\\s*([^;]+)`, 'g'), `${sv.setter}(prev => prev + $1)`);
|
|
665
|
+
converted = converted.replace(new RegExp(`${sv.name}\\s*-=\\s*([^;]+)`, 'g'), `${sv.setter}(prev => prev - $1)`);
|
|
666
|
+
converted = converted.replace(new RegExp(`${sv.name}\\+\\+`, 'g'), `${sv.setter}(prev => prev + 1)`);
|
|
667
|
+
converted = converted.replace(new RegExp(`${sv.name}--`, 'g'), `${sv.setter}(prev => prev - 1)`);
|
|
668
|
+
}
|
|
669
|
+
// Convert document.getElementById('x').value in variable declarations
|
|
670
|
+
// const name = document.getElementById('name').value → const name = new FormData(e.currentTarget).get('name') as string
|
|
671
|
+
converted = converted.replace(/(?:const|let|var)\s+(\w+)\s*=\s*document\.getElementById\(['"](\w+)['"]\)\.value\s*;?/g, 'const $1 = new FormData(e.currentTarget).get(\'$2\') as string;');
|
|
672
|
+
// Convert bare document.getElementById('x').value references (not in declarations)
|
|
673
|
+
converted = converted.replace(/document\.getElementById\(['"](\w+)['"]\)\.value/g, '(new FormData(e.currentTarget).get(\'$1\') as string || \'\')');
|
|
674
|
+
// Convert this.reset() (form context) to e.currentTarget.reset()
|
|
675
|
+
converted = converted.replace(/this\.reset\(\)/g, '(e.target as HTMLFormElement).reset()');
|
|
676
|
+
// Remove standalone document.getElementById references used for DOM manipulation (non-value reads)
|
|
677
|
+
converted = converted.replace(/(?:const|let|var)\s+\w+\s*=\s*document\.getElementById\(['"][^'"]+['"]\)\s*;?\n?/g, '');
|
|
678
|
+
return converted;
|
|
679
|
+
}
|
|
680
|
+
//# sourceMappingURL=js-analyzer.js.map
|