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,724 @@
|
|
|
1
|
+
import { htmlToJsx } from '../parsers/html-parser.js';
|
|
2
|
+
import { analyzeJS, convertEventHandlerToReact } from '../parsers/js-analyzer.js';
|
|
3
|
+
import { processCSS, mergeCSS } from '../parsers/css-processor.js';
|
|
4
|
+
import { toComponentName, toKebabCase, toValidIdentifier } from '../utils/naming.js';
|
|
5
|
+
import { implementPatterns } from './pattern-implementer.js';
|
|
6
|
+
/**
|
|
7
|
+
* Convert DOM mutations (classList.toggle, style changes, textContent) into
|
|
8
|
+
* React useState declarations where possible. Returns unconverted mutations
|
|
9
|
+
* as remaining for TODO comments.
|
|
10
|
+
*/
|
|
11
|
+
function convertDOMMutationsToState(mutations) {
|
|
12
|
+
const stateDeclarations = [];
|
|
13
|
+
const remaining = [];
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const dm of mutations) {
|
|
16
|
+
// classList.toggle('active') → [isActive, setIsActive] = useState(false)
|
|
17
|
+
if (dm.type === 'classList.toggle' || dm.type.startsWith('classList')) {
|
|
18
|
+
const className = dm.value.replace(/['"]/g, '');
|
|
19
|
+
// Sanitize class name to valid JS identifier (e.g., "text-[var(--primary)]" → "TextVarPrimary")
|
|
20
|
+
const sanitized = className
|
|
21
|
+
.replace(/\[.*?\]/g, '') // Remove bracket expressions like [var(--primary)]
|
|
22
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ') // Replace non-alphanumeric with spaces
|
|
23
|
+
.trim()
|
|
24
|
+
.split(/\s+/)
|
|
25
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
26
|
+
.join('');
|
|
27
|
+
if (!sanitized)
|
|
28
|
+
continue; // Skip if nothing remains after sanitization
|
|
29
|
+
const stateKey = `is${sanitized}`;
|
|
30
|
+
if (!seen.has(stateKey)) {
|
|
31
|
+
seen.add(stateKey);
|
|
32
|
+
stateDeclarations.push({
|
|
33
|
+
name: stateKey,
|
|
34
|
+
setter: `set${capitalize(stateKey)}`,
|
|
35
|
+
initial: 'false',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// style.property = value → state-driven inline styles
|
|
41
|
+
if (dm.type === 'style') {
|
|
42
|
+
const selector = toValidIdentifier(dm.selector);
|
|
43
|
+
const stateKey = `${selector}Style`;
|
|
44
|
+
if (!seen.has(stateKey)) {
|
|
45
|
+
seen.add(stateKey);
|
|
46
|
+
stateDeclarations.push({
|
|
47
|
+
name: stateKey,
|
|
48
|
+
setter: `set${capitalize(stateKey)}`,
|
|
49
|
+
initial: '{}',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// textContent mutations (e.g., counter updates) → state
|
|
55
|
+
if (dm.type === 'textContent') {
|
|
56
|
+
const stateKey = toValidIdentifier(dm.selector || 'text');
|
|
57
|
+
if (!seen.has(stateKey)) {
|
|
58
|
+
seen.add(stateKey);
|
|
59
|
+
stateDeclarations.push({
|
|
60
|
+
name: stateKey,
|
|
61
|
+
setter: `set${capitalize(stateKey)}`,
|
|
62
|
+
initial: "''",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// innerHTML with simple content → state
|
|
68
|
+
if (dm.type === 'innerHTML' && !dm.value.includes('<')) {
|
|
69
|
+
const stateKey = toValidIdentifier(dm.selector || 'content');
|
|
70
|
+
if (!seen.has(stateKey)) {
|
|
71
|
+
seen.add(stateKey);
|
|
72
|
+
stateDeclarations.push({
|
|
73
|
+
name: stateKey,
|
|
74
|
+
setter: `set${capitalize(stateKey)}`,
|
|
75
|
+
initial: "''",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
remaining.push(dm);
|
|
81
|
+
}
|
|
82
|
+
return { stateDeclarations, remaining };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Generate a React component from parsed HTML data.
|
|
86
|
+
* Optionally accepts a pre-computed analysis and custom hook info.
|
|
87
|
+
*/
|
|
88
|
+
export function generateComponent(parsed, cssModules, preAnalysis) {
|
|
89
|
+
const name = toComponentName(parsed.fileName);
|
|
90
|
+
const hooks = new Set();
|
|
91
|
+
// Process CSS
|
|
92
|
+
const allCSS = mergeCSS(parsed.inlineStyles);
|
|
93
|
+
const processed = processCSS(allCSS, cssModules);
|
|
94
|
+
// Analyze JavaScript (use pre-analysis if available)
|
|
95
|
+
const analyzed = preAnalysis || analyzeJS(parsed.inlineScripts.join('\n'));
|
|
96
|
+
// Determine needed hooks
|
|
97
|
+
if (analyzed.stateVars.length > 0)
|
|
98
|
+
hooks.add('useState');
|
|
99
|
+
if (analyzed.effects.length > 0 || analyzed.fetchCalls.length > 0)
|
|
100
|
+
hooks.add('useEffect');
|
|
101
|
+
if (analyzed.eventHandlers.some(h => h.elementSelector) || analyzed.refs.length > 0)
|
|
102
|
+
hooks.add('useRef');
|
|
103
|
+
if (analyzed.domMutations.length > 0)
|
|
104
|
+
hooks.add('useState');
|
|
105
|
+
// Convert HTML body to JSX
|
|
106
|
+
let jsx = htmlToJsx(parsed.bodyContent);
|
|
107
|
+
// Convert component placeholder divs to JSX component tags
|
|
108
|
+
jsx = jsx.replace(/<div data-component-placeholder="([^"]+)"><\/div>/g, (_, componentName) => `<${componentName} />`);
|
|
109
|
+
// Replace class names with CSS module references if using modules
|
|
110
|
+
if (cssModules && processed.classMap.size > 0) {
|
|
111
|
+
for (const [original, moduleName] of processed.classMap) {
|
|
112
|
+
jsx = jsx.replace(new RegExp(`className="${original}"`, 'g'), `className={styles.${moduleName}}`);
|
|
113
|
+
jsx = jsx.replace(new RegExp(`(className=")([^"]*\\b)${original}(\\b[^"]*")`, 'g'), (_, _pre, before, after) => {
|
|
114
|
+
const classes = `${before}${original}${after.slice(0, -1)}`.trim().split(/\s+/);
|
|
115
|
+
const moduleClasses = classes.map(c => {
|
|
116
|
+
const mapped = processed.classMap.get(c);
|
|
117
|
+
return mapped ? `styles.${mapped}` : `'${c}'`;
|
|
118
|
+
});
|
|
119
|
+
if (moduleClasses.length === 1)
|
|
120
|
+
return `className={${moduleClasses[0]}}`;
|
|
121
|
+
return `className={\`${moduleClasses.map(c => c.startsWith("'") ? c.slice(1, -1) : `\${${c}}`).join(' ')}\`}`;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const convertedHandlers = analyzed.eventHandlers.map(eh => ({
|
|
126
|
+
...eh,
|
|
127
|
+
handler: convertEventHandlerToReact(eh.handler, analyzed.stateVars),
|
|
128
|
+
}));
|
|
129
|
+
const cssFileName = cssModules
|
|
130
|
+
? `${toKebabCase(name)}.module.css`
|
|
131
|
+
: `${toKebabCase(name)}.css`;
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
jsx,
|
|
135
|
+
css: cssModules ? processed.moduleCSS : allCSS,
|
|
136
|
+
cssFileName,
|
|
137
|
+
hooks,
|
|
138
|
+
stateVars: analyzed.stateVars,
|
|
139
|
+
effects: analyzed.effects,
|
|
140
|
+
eventHandlers: convertedHandlers,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Render a full React component file from extracted component data.
|
|
145
|
+
*/
|
|
146
|
+
export function renderComponentFile(component, cssModules, customHooks = [], analysis, subComponentImports = []) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
// Pre-compute stubs so we know if useState is needed for imports
|
|
149
|
+
// (stubs are rendered later in the function body)
|
|
150
|
+
const preStubs = generateEventHandlerStubs(component.jsx, '');
|
|
151
|
+
const stubsNeedState = preStubs.some(s => s.includes('useState('));
|
|
152
|
+
// React imports — use a placeholder that we'll replace at the end
|
|
153
|
+
// because pattern implementations may add more hooks later
|
|
154
|
+
const allHooks = new Set(component.hooks);
|
|
155
|
+
if (analysis?.fetchCalls?.length)
|
|
156
|
+
allHooks.add('useEffect');
|
|
157
|
+
if (analysis?.fetchCalls?.length)
|
|
158
|
+
allHooks.add('useState');
|
|
159
|
+
if (stubsNeedState)
|
|
160
|
+
allHooks.add('useState');
|
|
161
|
+
const REACT_IMPORT_PLACEHOLDER = '___REACT_IMPORT_PLACEHOLDER___';
|
|
162
|
+
lines.push(REACT_IMPORT_PLACEHOLDER);
|
|
163
|
+
// CSS imports
|
|
164
|
+
if (component.css) {
|
|
165
|
+
if (cssModules) {
|
|
166
|
+
lines.push(`import styles from './${component.cssFileName}';`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
lines.push(`import './${component.cssFileName}';`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Sub-component imports
|
|
173
|
+
for (const subName of subComponentImports) {
|
|
174
|
+
const kebab = subName.charAt(0).toLowerCase() + subName.slice(1).replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
175
|
+
lines.push(`import ${subName} from './${kebab}';`);
|
|
176
|
+
}
|
|
177
|
+
// Custom hook imports
|
|
178
|
+
if (analysis) {
|
|
179
|
+
if (analysis.fetchCalls.length > 0 && customHooks.includes('useFetch')) {
|
|
180
|
+
lines.push(`import { useFetch } from '../hooks/useFetch';`);
|
|
181
|
+
}
|
|
182
|
+
if (analysis.storageCalls.some(s => s.storageType === 'localStorage') && customHooks.includes('useLocalStorage')) {
|
|
183
|
+
lines.push(`import { useLocalStorage } from '../hooks/useLocalStorage';`);
|
|
184
|
+
}
|
|
185
|
+
if (analysis.storageCalls.some(s => s.storageType === 'sessionStorage') && customHooks.includes('useSessionStorage')) {
|
|
186
|
+
lines.push(`import { useSessionStorage } from '../hooks/useSessionStorage';`);
|
|
187
|
+
}
|
|
188
|
+
if (analysis.timerCalls.some(t => t.type === 'setInterval') && customHooks.includes('useInterval')) {
|
|
189
|
+
lines.push(`import { useInterval } from '../hooks/useInterval';`);
|
|
190
|
+
}
|
|
191
|
+
if (analysis.timerCalls.some(t => t.type === 'setTimeout') && customHooks.includes('useTimeout')) {
|
|
192
|
+
lines.push(`import { useTimeout } from '../hooks/useTimeout';`);
|
|
193
|
+
}
|
|
194
|
+
// Interactive pattern hook imports
|
|
195
|
+
const ip = analysis.interactivePatterns;
|
|
196
|
+
if (ip?.scrollReveals?.length && customHooks.includes('useScrollReveal')) {
|
|
197
|
+
lines.push(`import { useScrollReveal } from '../hooks/useScrollReveal';`);
|
|
198
|
+
}
|
|
199
|
+
if (ip?.scrollClasses?.length && customHooks.includes('useScrollClass')) {
|
|
200
|
+
lines.push(`import { useScrollClass } from '../hooks/useScrollClass';`);
|
|
201
|
+
}
|
|
202
|
+
if (ip?.counterAnimations?.length && customHooks.includes('useCounterAnimation')) {
|
|
203
|
+
lines.push(`import { useCounterAnimation } from '../hooks/useCounterAnimation';`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
lines.push('');
|
|
207
|
+
// Component function
|
|
208
|
+
lines.push(`function ${component.name}() {`);
|
|
209
|
+
// State declarations
|
|
210
|
+
for (const sv of component.stateVars) {
|
|
211
|
+
lines.push(` const [${sv.name}, ${sv.setter}] = useState(${sv.initialValue});`);
|
|
212
|
+
}
|
|
213
|
+
if (component.stateVars.length > 0)
|
|
214
|
+
lines.push('');
|
|
215
|
+
// Interactive pattern hook calls
|
|
216
|
+
if (analysis?.interactivePatterns) {
|
|
217
|
+
const ip = analysis.interactivePatterns;
|
|
218
|
+
if (ip.scrollReveals.length > 0 && customHooks.includes('useScrollReveal')) {
|
|
219
|
+
for (const sr of ip.scrollReveals) {
|
|
220
|
+
const mode = sr.property === 'animationPlayState' ? 'animation' : 'class';
|
|
221
|
+
const className = sr.property === 'classList' ? `, '${sr.activeValue}'` : '';
|
|
222
|
+
lines.push(` useScrollReveal('${sr.selector}', ${sr.threshold}, '${mode}'${className});`);
|
|
223
|
+
}
|
|
224
|
+
lines.push('');
|
|
225
|
+
}
|
|
226
|
+
if (ip.scrollClasses.length > 0 && customHooks.includes('useScrollClass')) {
|
|
227
|
+
for (const sc of ip.scrollClasses) {
|
|
228
|
+
lines.push(` useScrollClass('${sc.elementSelector}', '${sc.className}', ${sc.threshold});`);
|
|
229
|
+
}
|
|
230
|
+
lines.push('');
|
|
231
|
+
}
|
|
232
|
+
if (ip.counterAnimations.length > 0 && customHooks.includes('useCounterAnimation')) {
|
|
233
|
+
for (const ca of ip.counterAnimations) {
|
|
234
|
+
lines.push(` useCounterAnimation('${ca.selector}');`);
|
|
235
|
+
}
|
|
236
|
+
lines.push('');
|
|
237
|
+
}
|
|
238
|
+
if (ip.tabs.length > 0) {
|
|
239
|
+
allHooks.add('useState');
|
|
240
|
+
for (let i = 0; i < ip.tabs.length; i++) {
|
|
241
|
+
const suffix = ip.tabs.length > 1 ? String(i + 1) : '';
|
|
242
|
+
lines.push(` const [activeTab${suffix}, setActiveTab${suffix}] = useState(0);`);
|
|
243
|
+
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
if (ip.accordions.length > 0) {
|
|
247
|
+
allHooks.add('useState');
|
|
248
|
+
for (let i = 0; i < ip.accordions.length; i++) {
|
|
249
|
+
const suffix = ip.accordions.length > 1 ? String(i + 1) : '';
|
|
250
|
+
lines.push(` const [openAccordion${suffix}, setOpenAccordion${suffix}] = useState<number | null>(null);`);
|
|
251
|
+
}
|
|
252
|
+
lines.push('');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Ref declarations
|
|
256
|
+
if (analysis?.refs?.length) {
|
|
257
|
+
for (const ref of analysis.refs) {
|
|
258
|
+
lines.push(` const ${ref.name} = useRef<${ref.type}>(null);`);
|
|
259
|
+
}
|
|
260
|
+
lines.push('');
|
|
261
|
+
}
|
|
262
|
+
// Fetch calls -> useFetch or useEffect+fetch
|
|
263
|
+
if (analysis?.fetchCalls?.length) {
|
|
264
|
+
for (const fc of analysis.fetchCalls) {
|
|
265
|
+
if (customHooks.includes('useFetch')) {
|
|
266
|
+
const varName = fc.variableName || 'data';
|
|
267
|
+
lines.push(` const { data: ${varName}, loading, error } = useFetch('${fc.url}');`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
lines.push(` const [fetchData, setFetchData] = useState<unknown>(null);`);
|
|
271
|
+
lines.push(` const [fetchLoading, setFetchLoading] = useState(true);`);
|
|
272
|
+
lines.push('');
|
|
273
|
+
lines.push(' useEffect(() => {');
|
|
274
|
+
lines.push(` fetch('${fc.url}'${fc.method !== 'GET' ? `, { method: '${fc.method}' }` : ''})`);
|
|
275
|
+
lines.push(' .then(res => res.json())');
|
|
276
|
+
lines.push(' .then(data => { setFetchData(data); setFetchLoading(false); })');
|
|
277
|
+
lines.push(' .catch(err => { console.error(err); setFetchLoading(false); });');
|
|
278
|
+
lines.push(' }, []);');
|
|
279
|
+
}
|
|
280
|
+
lines.push('');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Storage calls -> custom hooks
|
|
284
|
+
if (analysis?.storageCalls?.length) {
|
|
285
|
+
const processedKeys = new Set();
|
|
286
|
+
for (const sc of analysis.storageCalls) {
|
|
287
|
+
if (sc.operation === 'getItem' && !processedKeys.has(sc.key)) {
|
|
288
|
+
processedKeys.add(sc.key);
|
|
289
|
+
const varName = toSafeVarName(sc.key);
|
|
290
|
+
const hookName = sc.storageType === 'localStorage' ? 'useLocalStorage' : 'useSessionStorage';
|
|
291
|
+
if (customHooks.includes(hookName)) {
|
|
292
|
+
lines.push(` const [${varName}, set${capitalize(varName)}] = ${hookName}('${sc.key}', null);`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (processedKeys.size > 0)
|
|
297
|
+
lines.push('');
|
|
298
|
+
}
|
|
299
|
+
// Effects
|
|
300
|
+
for (const effect of component.effects) {
|
|
301
|
+
lines.push(' useEffect(() => {');
|
|
302
|
+
for (const line of effect.split('\n')) {
|
|
303
|
+
lines.push(` ${line}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push(' }, []);');
|
|
306
|
+
lines.push('');
|
|
307
|
+
}
|
|
308
|
+
// Timer calls -> custom hooks or inline useEffect
|
|
309
|
+
// Filter out timers that contain DOM manipulation (they can't run in React)
|
|
310
|
+
const safeTimerCalls = (analysis?.timerCalls || []).filter(tc => {
|
|
311
|
+
return !/\.classList\.|\.innerHTML|\.textContent|\.style\.|\.remove\(\)|document\.|\.appendChild/.test(tc.handler);
|
|
312
|
+
});
|
|
313
|
+
if (safeTimerCalls.length > 0) {
|
|
314
|
+
for (const tc of safeTimerCalls) {
|
|
315
|
+
if (tc.type === 'setInterval' && customHooks.includes('useInterval')) {
|
|
316
|
+
lines.push(` useInterval(() => {`);
|
|
317
|
+
for (const line of tc.handler.split('\n')) {
|
|
318
|
+
if (line.trim())
|
|
319
|
+
lines.push(` ${line.trim()}`);
|
|
320
|
+
}
|
|
321
|
+
lines.push(` }, ${tc.delay});`);
|
|
322
|
+
}
|
|
323
|
+
else if (tc.type === 'setTimeout' && customHooks.includes('useTimeout')) {
|
|
324
|
+
lines.push(` useTimeout(() => {`);
|
|
325
|
+
for (const line of tc.handler.split('\n')) {
|
|
326
|
+
if (line.trim())
|
|
327
|
+
lines.push(` ${line.trim()}`);
|
|
328
|
+
}
|
|
329
|
+
lines.push(` }, ${tc.delay});`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
lines.push(' useEffect(() => {');
|
|
333
|
+
lines.push(` const id = ${tc.type}(() => {`);
|
|
334
|
+
for (const line of tc.handler.split('\n')) {
|
|
335
|
+
if (line.trim())
|
|
336
|
+
lines.push(` ${line.trim()}`);
|
|
337
|
+
}
|
|
338
|
+
lines.push(` }, ${tc.delay});`);
|
|
339
|
+
lines.push(` return () => clear${tc.type === 'setTimeout' ? 'Timeout' : 'Interval'}(id);`);
|
|
340
|
+
lines.push(' }, []);');
|
|
341
|
+
}
|
|
342
|
+
lines.push('');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Event handler functions
|
|
346
|
+
for (const eh of component.eventHandlers) {
|
|
347
|
+
const handlerName = `handle${capitalize(eh.event)}`;
|
|
348
|
+
// Add event parameter for submit, change, input, keydown, etc.
|
|
349
|
+
const needsEvent = ['submit', 'change', 'input', 'keydown', 'keyup', 'keypress'].includes(eh.event);
|
|
350
|
+
const eventTypeMap = {
|
|
351
|
+
submit: 'React.FormEvent<HTMLFormElement>',
|
|
352
|
+
change: 'React.ChangeEvent<HTMLInputElement>',
|
|
353
|
+
input: 'React.ChangeEvent<HTMLInputElement>',
|
|
354
|
+
keydown: 'React.KeyboardEvent<HTMLElement>',
|
|
355
|
+
keyup: 'React.KeyboardEvent<HTMLElement>',
|
|
356
|
+
keypress: 'React.KeyboardEvent<HTMLElement>',
|
|
357
|
+
click: 'React.MouseEvent<HTMLElement>',
|
|
358
|
+
focus: 'React.FocusEvent<HTMLElement>',
|
|
359
|
+
blur: 'React.FocusEvent<HTMLElement>',
|
|
360
|
+
};
|
|
361
|
+
const eventType = eventTypeMap[eh.event] || 'React.SyntheticEvent';
|
|
362
|
+
const paramStr = needsEvent ? `(e: ${eventType})` : '()';
|
|
363
|
+
lines.push(` const ${handlerName} = ${paramStr} => {`);
|
|
364
|
+
for (const line of eh.handler.split('\n')) {
|
|
365
|
+
const trimmed = line.trim();
|
|
366
|
+
if (!trimmed)
|
|
367
|
+
continue;
|
|
368
|
+
// Convert classList.toggle('x') to React state toggle
|
|
369
|
+
const toggleMatch = trimmed.match(/\w+\.classList\.toggle\(['"]([^'"]+)['"]\)/);
|
|
370
|
+
if (toggleMatch) {
|
|
371
|
+
const cls = toggleMatch[1]
|
|
372
|
+
.replace(/\[.*?\]/g, '')
|
|
373
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
374
|
+
.trim()
|
|
375
|
+
.split(/\s+/)
|
|
376
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
377
|
+
.join('');
|
|
378
|
+
if (cls) {
|
|
379
|
+
lines.push(` setIs${cls}(prev => !prev);`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Skip other raw DOM manipulation lines that would cause TS errors
|
|
384
|
+
if (/\.\s*(classList|style|innerHTML|textContent|appendChild|removeChild)\b/.test(trimmed) ||
|
|
385
|
+
/document\.(getElementById|querySelector|getElementsBy)/.test(trimmed)) {
|
|
386
|
+
lines.push(` // TODO: ${trimmed}`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
lines.push(` ${trimmed}`);
|
|
390
|
+
}
|
|
391
|
+
lines.push(' };');
|
|
392
|
+
lines.push('');
|
|
393
|
+
}
|
|
394
|
+
// jQuery pattern warnings as comments
|
|
395
|
+
if (analysis?.jqueryPatterns?.length) {
|
|
396
|
+
lines.push(' // TODO: The following jQuery patterns were detected and need manual review:');
|
|
397
|
+
for (const jp of analysis.jqueryPatterns) {
|
|
398
|
+
lines.push(` // jQuery: $('${jp.selector}').${jp.method}(${jp.args})`);
|
|
399
|
+
}
|
|
400
|
+
lines.push('');
|
|
401
|
+
}
|
|
402
|
+
// Convert DOM mutations to React state + effects where possible
|
|
403
|
+
if (analysis?.domMutations?.length) {
|
|
404
|
+
const converted = convertDOMMutationsToState(analysis.domMutations);
|
|
405
|
+
if (converted.stateDeclarations.length > 0) {
|
|
406
|
+
for (const decl of converted.stateDeclarations) {
|
|
407
|
+
// Only add if not already declared
|
|
408
|
+
if (!lines.some(l => l.includes(`const [${decl.name},`))) {
|
|
409
|
+
lines.splice(lines.findIndex(l => l.includes('return (')) || lines.length, 0, ` const [${decl.name}, ${decl.setter}] = useState(${decl.initial});`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
lines.push('');
|
|
413
|
+
}
|
|
414
|
+
// Add remaining unconverted mutations as TODO comments
|
|
415
|
+
if (converted.remaining.length > 0) {
|
|
416
|
+
lines.push(' // TODO: DOM mutations detected — consider converting to React state:');
|
|
417
|
+
for (const dm of converted.remaining) {
|
|
418
|
+
const valueLine = dm.value.split('\n')[0].trim();
|
|
419
|
+
const suffix = dm.value.includes('\n') ? ' ...' : '';
|
|
420
|
+
lines.push(` // ${dm.type}: ${dm.selector} = ${valueLine}${suffix}`);
|
|
421
|
+
}
|
|
422
|
+
lines.push('');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// ─── PATTERN IMPLEMENTATION ENGINE ─────────────────────────────────
|
|
426
|
+
// Apply detected interactive patterns to generate real working React code
|
|
427
|
+
// instead of stubs. This modifies the JSX and adds state/handlers.
|
|
428
|
+
let patternJsx = component.jsx;
|
|
429
|
+
if (analysis?.interactivePatterns) {
|
|
430
|
+
const patternResult = implementPatterns(patternJsx, analysis.interactivePatterns, customHooks);
|
|
431
|
+
patternJsx = patternResult.jsx;
|
|
432
|
+
// Add pattern state declarations (before return, after existing state)
|
|
433
|
+
for (const decl of patternResult.stateDeclarations) {
|
|
434
|
+
// Skip if already declared
|
|
435
|
+
const varName = decl.match(/\[(\w+),/)?.[1];
|
|
436
|
+
if (varName && lines.some(l => l.includes(`const [${varName},`)))
|
|
437
|
+
continue;
|
|
438
|
+
lines.push(` ${decl}`);
|
|
439
|
+
}
|
|
440
|
+
if (patternResult.stateDeclarations.length > 0)
|
|
441
|
+
lines.push('');
|
|
442
|
+
// Add pattern handlers
|
|
443
|
+
for (const handler of patternResult.handlers) {
|
|
444
|
+
// Skip if already declared
|
|
445
|
+
const fnName = handler.match(/const\s+(\w+)\s*=/)?.[1];
|
|
446
|
+
if (fnName && lines.some(l => l.includes(`const ${fnName}`)))
|
|
447
|
+
continue;
|
|
448
|
+
lines.push(` ${handler}`);
|
|
449
|
+
lines.push('');
|
|
450
|
+
}
|
|
451
|
+
// Add pattern effects
|
|
452
|
+
for (const effect of patternResult.effects) {
|
|
453
|
+
lines.push(' useEffect(() => {');
|
|
454
|
+
lines.push(` ${effect}`);
|
|
455
|
+
lines.push(' }, []);');
|
|
456
|
+
lines.push('');
|
|
457
|
+
allHooks.add('useEffect');
|
|
458
|
+
}
|
|
459
|
+
// Add any additional hooks needed
|
|
460
|
+
for (const hook of patternResult.additionalHooks) {
|
|
461
|
+
allHooks.add(hook);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Generate stub functions for undefined references in JSX event handlers
|
|
465
|
+
// Also scan handler bodies for function calls that need stubs (e.g., showToast)
|
|
466
|
+
const handlerBodies = component.eventHandlers.map(eh => eh.handler).join('\n');
|
|
467
|
+
const stubs = generateEventHandlerStubs(patternJsx + '\n' + handlerBodies, lines.join('\n'));
|
|
468
|
+
if (stubs.length > 0) {
|
|
469
|
+
lines.push(' // Auto-generated stub functions for inline event handlers');
|
|
470
|
+
for (const stub of stubs) {
|
|
471
|
+
lines.push(stub);
|
|
472
|
+
}
|
|
473
|
+
lines.push('');
|
|
474
|
+
}
|
|
475
|
+
// Post-process JSX: add key props to list items
|
|
476
|
+
let finalJsx = addKeyPropsToLists(patternJsx);
|
|
477
|
+
// Detect <Link> usage and add react-router-dom import
|
|
478
|
+
const usesLink = /<Link\b/.test(finalJsx);
|
|
479
|
+
if (usesLink) {
|
|
480
|
+
// Insert import after the React import placeholder
|
|
481
|
+
const placeholderIdx = lines.indexOf(REACT_IMPORT_PLACEHOLDER);
|
|
482
|
+
if (placeholderIdx >= 0) {
|
|
483
|
+
lines.splice(placeholderIdx + 1, 0, `import { Link } from 'react-router-dom';`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// JSX return
|
|
487
|
+
lines.push(' return (');
|
|
488
|
+
lines.push(' <>');
|
|
489
|
+
for (const line of finalJsx.split('\n')) {
|
|
490
|
+
lines.push(` ${line}`);
|
|
491
|
+
}
|
|
492
|
+
lines.push(' </>');
|
|
493
|
+
lines.push(' );');
|
|
494
|
+
lines.push('}');
|
|
495
|
+
lines.push('');
|
|
496
|
+
// Use React.memo for sub-components (not the main page component)
|
|
497
|
+
const useReactMemo = subComponentImports.length === 0 && component.name !== 'Index';
|
|
498
|
+
if (useReactMemo) {
|
|
499
|
+
lines.push(`export default React.memo(${component.name});`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
lines.push(`export default ${component.name};`);
|
|
503
|
+
}
|
|
504
|
+
// Utility functions — placed outside the component as commented-out reference code
|
|
505
|
+
// since they typically contain DOM manipulation that needs manual React conversion
|
|
506
|
+
if (analysis?.utilityCode?.length) {
|
|
507
|
+
lines.push('');
|
|
508
|
+
lines.push('/*');
|
|
509
|
+
lines.push(' * TODO: The following utility functions were extracted from the original JavaScript.');
|
|
510
|
+
lines.push(' * They contain DOM manipulation and need to be converted to React patterns.');
|
|
511
|
+
lines.push(' *');
|
|
512
|
+
for (const fn of analysis.utilityCode) {
|
|
513
|
+
for (const line of fn.split('\n')) {
|
|
514
|
+
lines.push(` * ${line}`);
|
|
515
|
+
}
|
|
516
|
+
lines.push(' *');
|
|
517
|
+
}
|
|
518
|
+
lines.push(' */');
|
|
519
|
+
}
|
|
520
|
+
lines.push('');
|
|
521
|
+
// Resolve the React import placeholder now that all hooks are known
|
|
522
|
+
const placeholderIdx = lines.indexOf(REACT_IMPORT_PLACEHOLDER);
|
|
523
|
+
if (placeholderIdx >= 0) {
|
|
524
|
+
if (allHooks.size > 0 && useReactMemo) {
|
|
525
|
+
const hookImports = Array.from(allHooks).join(', ');
|
|
526
|
+
lines[placeholderIdx] = `import React, { ${hookImports} } from 'react';`;
|
|
527
|
+
}
|
|
528
|
+
else if (allHooks.size > 0) {
|
|
529
|
+
const hookImports = Array.from(allHooks).join(', ');
|
|
530
|
+
lines[placeholderIdx] = `import { ${hookImports} } from 'react';`;
|
|
531
|
+
}
|
|
532
|
+
else if (useReactMemo) {
|
|
533
|
+
lines[placeholderIdx] = `import React from 'react';`;
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
lines[placeholderIdx] = '';
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return lines.join('\n');
|
|
540
|
+
}
|
|
541
|
+
function capitalize(s) {
|
|
542
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
543
|
+
}
|
|
544
|
+
function toSafeVarName(key) {
|
|
545
|
+
return key.replace(/[^a-zA-Z0-9]/g, '_').replace(/^_+|_+$/g, '') || 'value';
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Add key props to repeated list items (<li>, <tr>, <option>) inside their parent containers.
|
|
549
|
+
* This prevents React "key" warnings.
|
|
550
|
+
*/
|
|
551
|
+
function addKeyPropsToLists(jsx) {
|
|
552
|
+
// Add key to <li> elements inside <ul> or <ol>
|
|
553
|
+
let keyCounter = 0;
|
|
554
|
+
return jsx.replace(/<(li|tr|option)(\s[^>]*?)?(\/?)>/g, (match, tag, attrs, selfClose) => {
|
|
555
|
+
attrs = attrs || '';
|
|
556
|
+
// Skip if already has a key prop
|
|
557
|
+
if (/\bkey=/.test(attrs))
|
|
558
|
+
return match;
|
|
559
|
+
keyCounter++;
|
|
560
|
+
return `<${tag}${attrs} key="${tag}-${keyCounter}"${selfClose}>`;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Scan JSX for function calls in event handlers and generate stubs
|
|
565
|
+
* for any that aren't already defined in the component body.
|
|
566
|
+
*/
|
|
567
|
+
function generateEventHandlerStubs(jsx, existingCode) {
|
|
568
|
+
const stubs = [];
|
|
569
|
+
const generated = new Set();
|
|
570
|
+
// Strip content inside <pre>, <code>, <textarea>, and <script> to avoid
|
|
571
|
+
// generating stubs for function calls in displayed code examples
|
|
572
|
+
const strippedJsx = jsx
|
|
573
|
+
.replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, '<pre></pre>')
|
|
574
|
+
.replace(/<code[^>]*>[\s\S]*?<\/code>/gi, '<code></code>')
|
|
575
|
+
.replace(/<textarea[^>]*>[\s\S]*?<\/textarea>/gi, '<textarea></textarea>');
|
|
576
|
+
// Find all function calls inside event handlers: onClick={() => { funcName(...) }}
|
|
577
|
+
const handlerPattern = /on[A-Z][a-zA-Z]*=\{[^}]*?(\w+)\s*\(/g;
|
|
578
|
+
let match;
|
|
579
|
+
while ((match = handlerPattern.exec(strippedJsx)) !== null) {
|
|
580
|
+
const funcName = match[1];
|
|
581
|
+
// Skip common built-in/known names
|
|
582
|
+
if (['console', 'window', 'document', 'Math', 'JSON', 'Array', 'Object',
|
|
583
|
+
'parseInt', 'parseFloat', 'setTimeout', 'setInterval', 'clearTimeout',
|
|
584
|
+
'clearInterval', 'alert', 'confirm', 'prompt', 'fetch', 'event', 'e',
|
|
585
|
+
'if', 'for', 'while', 'switch', 'return', 'function', 'const', 'let', 'var',
|
|
586
|
+
'typeof', 'instanceof', 'void', 'delete', 'new', 'this', 'class', 'super',
|
|
587
|
+
'set', 'get', 'String', 'Number', 'Boolean', 'Date', 'Promise',
|
|
588
|
+
'preventDefault', 'stopPropagation', 'reset', 'FormData', 'Error',
|
|
589
|
+
'requestAnimationFrame', 'cancelAnimationFrame', 'require',
|
|
590
|
+
'resolve', 'reject', 'then', 'catch', 'map', 'filter', 'reduce',
|
|
591
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat',
|
|
592
|
+
'join', 'split', 'trim', 'replace', 'match', 'test', 'exec',
|
|
593
|
+
'find', 'findIndex', 'every', 'some', 'includes', 'indexOf',
|
|
594
|
+
'keys', 'values', 'entries', 'assign', 'freeze', 'create',
|
|
595
|
+
'from', 'isArray', 'isNaN', 'isFinite', 'encodeURIComponent',
|
|
596
|
+
'decodeURIComponent', 'encodeURI', 'decodeURI', 'atob', 'btoa',
|
|
597
|
+
'useScrollReveal', 'useScrollClass', 'useCounterAnimation',
|
|
598
|
+
'useInterval', 'useTimeout', 'useFetch', 'useLocalStorage',
|
|
599
|
+
'useRef', 'useState', 'useEffect', 'useCallback', 'useMemo',
|
|
600
|
+
'useContext', 'useReducer', 'useLayoutEffect',
|
|
601
|
+
'checkValidity', 'reportValidity', 'setCustomValidity',
|
|
602
|
+
'closest', 'contains', 'getAttribute', 'setAttribute',
|
|
603
|
+
'classList', 'getComputedStyle', 'getBoundingClientRect',
|
|
604
|
+
'scrollTo', 'scrollIntoView', 'focus', 'blur', 'click',
|
|
605
|
+
'dispatchEvent', 'removeAttribute', 'hasAttribute',
|
|
606
|
+
].includes(funcName))
|
|
607
|
+
continue;
|
|
608
|
+
// Skip if it starts with 'set' (likely a useState setter)
|
|
609
|
+
if (/^set[A-Z]/.test(funcName))
|
|
610
|
+
continue;
|
|
611
|
+
// Skip if already defined in the component
|
|
612
|
+
if (existingCode.includes(`const ${funcName}`) ||
|
|
613
|
+
existingCode.includes(`function ${funcName}`) ||
|
|
614
|
+
existingCode.includes(`let ${funcName}`))
|
|
615
|
+
continue;
|
|
616
|
+
// Skip duplicates
|
|
617
|
+
if (generated.has(funcName))
|
|
618
|
+
continue;
|
|
619
|
+
generated.add(funcName);
|
|
620
|
+
// Generate a sensible stub based on the function name pattern
|
|
621
|
+
const stub = generateStubForFunction(funcName);
|
|
622
|
+
stubs.push(stub);
|
|
623
|
+
}
|
|
624
|
+
// Also scan for function calls in handler bodies and JSX expressions
|
|
625
|
+
// Match standalone function calls (not method calls preceded by '.')
|
|
626
|
+
const bareCallPattern = /(?<![.\w])([a-zA-Z_]\w*)\s*\(/g;
|
|
627
|
+
while ((match = bareCallPattern.exec(strippedJsx)) !== null) {
|
|
628
|
+
const funcName = match[1];
|
|
629
|
+
if (generated.has(funcName))
|
|
630
|
+
continue;
|
|
631
|
+
// Skip built-ins, keywords, common methods
|
|
632
|
+
if (['console', 'window', 'document', 'Math', 'JSON', 'setTimeout',
|
|
633
|
+
'clearTimeout', 'setInterval', 'clearInterval', 'alert', 'fetch',
|
|
634
|
+
'preventDefault', 'stopPropagation', 'reset', 'FormData', 'Error',
|
|
635
|
+
'requestAnimationFrame', 'cancelAnimationFrame', 'new', 'require',
|
|
636
|
+
'resolve', 'reject', 'then', 'catch', 'map', 'filter', 'reduce',
|
|
637
|
+
'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat',
|
|
638
|
+
'join', 'split', 'trim', 'replace', 'match', 'test', 'exec',
|
|
639
|
+
'find', 'findIndex', 'every', 'some', 'includes', 'indexOf',
|
|
640
|
+
'keys', 'values', 'entries', 'assign', 'freeze', 'create',
|
|
641
|
+
'from', 'isArray', 'isNaN', 'isFinite', 'Promise', 'Date',
|
|
642
|
+
'if', 'for', 'while', 'switch', 'return', 'function', 'const',
|
|
643
|
+
'let', 'var', 'typeof', 'instanceof', 'void', 'delete',
|
|
644
|
+
'addEventListener', 'removeEventListener', 'querySelector',
|
|
645
|
+
'querySelectorAll', 'getElementById', 'getElementsByClassName',
|
|
646
|
+
'createElement', 'appendChild', 'removeChild', 'insertBefore',
|
|
647
|
+
'observe', 'unobserve', 'disconnect', 'IntersectionObserver',
|
|
648
|
+
'MutationObserver', 'ResizeObserver', 'AbortController',
|
|
649
|
+
'encodeURIComponent', 'decodeURIComponent', 'atob', 'btoa',
|
|
650
|
+
'useScrollReveal', 'useScrollClass', 'useCounterAnimation',
|
|
651
|
+
'useInterval', 'useTimeout', 'useFetch', 'useLocalStorage',
|
|
652
|
+
'useRef', 'useState', 'useEffect', 'useCallback', 'useMemo',
|
|
653
|
+
'checkValidity', 'reportValidity', 'setCustomValidity',
|
|
654
|
+
'closest', 'contains', 'getAttribute', 'setAttribute',
|
|
655
|
+
'getComputedStyle', 'getBoundingClientRect', 'scrollTo',
|
|
656
|
+
'scrollIntoView', 'focus', 'blur', 'click', 'dispatchEvent',
|
|
657
|
+
].includes(funcName))
|
|
658
|
+
continue;
|
|
659
|
+
// Skip React/state setters
|
|
660
|
+
if (/^set[A-Z]/.test(funcName))
|
|
661
|
+
continue;
|
|
662
|
+
// Skip constructor-like calls (start uppercase, not a known pattern)
|
|
663
|
+
if (/^[A-Z]/.test(funcName) && !funcName.includes('Toast') && !funcName.includes('Modal'))
|
|
664
|
+
continue;
|
|
665
|
+
// Skip if already defined in the component
|
|
666
|
+
if (existingCode.includes(`const ${funcName}`) ||
|
|
667
|
+
existingCode.includes(`function ${funcName}`) ||
|
|
668
|
+
existingCode.includes(`let ${funcName}`))
|
|
669
|
+
continue;
|
|
670
|
+
generated.add(funcName);
|
|
671
|
+
stubs.push(generateStubForFunction(funcName));
|
|
672
|
+
}
|
|
673
|
+
return stubs;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Generate a sensible stub implementation based on function name patterns.
|
|
677
|
+
*/
|
|
678
|
+
function generateStubForFunction(name) {
|
|
679
|
+
const lower = name.toLowerCase();
|
|
680
|
+
// Navigation
|
|
681
|
+
if (lower === 'navigate' || lower === 'goto' || lower === 'redirect') {
|
|
682
|
+
return ` const ${name} = (path: string) => { window.location.hash = path; };`;
|
|
683
|
+
}
|
|
684
|
+
// Close/open modal patterns
|
|
685
|
+
if (lower.startsWith('close') || lower.startsWith('hide')) {
|
|
686
|
+
const target = name.replace(/^(close|hide)/i, '');
|
|
687
|
+
const stateVar = target.charAt(0).toLowerCase() + target.slice(1) + 'Open';
|
|
688
|
+
return ` const [${stateVar}, set${capitalize(stateVar)}] = useState(false);\n const ${name} = () => { set${capitalize(stateVar)}(false); };`;
|
|
689
|
+
}
|
|
690
|
+
// Toast/notification pattern (check before generic show/open)
|
|
691
|
+
if (lower.includes('toast') || lower.includes('notify') || lower.includes('notification') || lower.includes('snackbar') || lower.includes('alert') || lower.includes('flash')) {
|
|
692
|
+
return ` const [toastMessage, setToastMessage] = useState('');\n const ${name} = (message: string) => {\n setToastMessage(message);\n setTimeout(() => setToastMessage(''), 3000);\n };`;
|
|
693
|
+
}
|
|
694
|
+
if (lower.startsWith('open') || lower.startsWith('show')) {
|
|
695
|
+
const target = name.replace(/^(open|show)/i, '');
|
|
696
|
+
const stateVar = target.charAt(0).toLowerCase() + target.slice(1) + 'Open';
|
|
697
|
+
return ` const [${stateVar}, set${capitalize(stateVar)}] = useState(false);\n const ${name} = (..._args: any[]) => { set${capitalize(stateVar)}(true); };`;
|
|
698
|
+
}
|
|
699
|
+
// Toggle patterns
|
|
700
|
+
if (lower.startsWith('toggle')) {
|
|
701
|
+
const target = name.replace(/^toggle/i, '');
|
|
702
|
+
const stateVar = target.charAt(0).toLowerCase() + target.slice(1) + 'Open';
|
|
703
|
+
return ` const [${stateVar}, set${capitalize(stateVar)}] = useState(false);\n const ${name} = () => { set${capitalize(stateVar)}(prev => !prev); };`;
|
|
704
|
+
}
|
|
705
|
+
// Handle/on patterns
|
|
706
|
+
if (lower.startsWith('handle') || lower.startsWith('on')) {
|
|
707
|
+
return ` const ${name} = (..._args: any[]) => { /* TODO: implement ${name} */ };`;
|
|
708
|
+
}
|
|
709
|
+
// prev/next patterns (e.g., prevLightboxImage, nextLightboxImage)
|
|
710
|
+
if (lower.startsWith('prev') || lower.startsWith('next') || lower.startsWith('go')) {
|
|
711
|
+
return ` const ${name} = () => { /* TODO: implement ${name} */ };`;
|
|
712
|
+
}
|
|
713
|
+
// Filter/sort patterns
|
|
714
|
+
if (lower.startsWith('filter') || lower.startsWith('sort') || lower.startsWith('search')) {
|
|
715
|
+
return ` const ${name} = (..._args: any[]) => { /* TODO: implement ${name} */ };`;
|
|
716
|
+
}
|
|
717
|
+
// Submit/send pattern
|
|
718
|
+
if (lower.startsWith('submit') || lower.startsWith('send')) {
|
|
719
|
+
return ` const ${name} = (e: React.FormEvent) => {\n e.preventDefault();\n /* TODO: implement ${name} */\n };`;
|
|
720
|
+
}
|
|
721
|
+
// Generic fallback
|
|
722
|
+
return ` const ${name} = (..._args: any[]) => { /* TODO: implement ${name} */ };`;
|
|
723
|
+
}
|
|
724
|
+
//# sourceMappingURL=component-generator.js.map
|