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.
Files changed (243) hide show
  1. package/README.md +514 -0
  2. package/bin/cli.js +2 -0
  3. package/dist/ai/agent.d.ts +124 -0
  4. package/dist/ai/agent.d.ts.map +1 -0
  5. package/dist/ai/agent.js +289 -0
  6. package/dist/ai/agent.js.map +1 -0
  7. package/dist/ai/index.d.ts +10 -0
  8. package/dist/ai/index.d.ts.map +1 -0
  9. package/dist/ai/index.js +10 -0
  10. package/dist/ai/index.js.map +1 -0
  11. package/dist/ai/prompts.d.ts +35 -0
  12. package/dist/ai/prompts.d.ts.map +1 -0
  13. package/dist/ai/prompts.js +166 -0
  14. package/dist/ai/prompts.js.map +1 -0
  15. package/dist/ai/refinement-loop.d.ts +29 -0
  16. package/dist/ai/refinement-loop.d.ts.map +1 -0
  17. package/dist/ai/refinement-loop.js +180 -0
  18. package/dist/ai/refinement-loop.js.map +1 -0
  19. package/dist/ai/tools.d.ts +17 -0
  20. package/dist/ai/tools.d.ts.map +1 -0
  21. package/dist/ai/tools.js +353 -0
  22. package/dist/ai/tools.js.map +1 -0
  23. package/dist/ai/visual-compare.d.ts +43 -0
  24. package/dist/ai/visual-compare.d.ts.map +1 -0
  25. package/dist/ai/visual-compare.js +176 -0
  26. package/dist/ai/visual-compare.js.map +1 -0
  27. package/dist/cli.d.ts +3 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +179 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/converter.d.ts +10 -0
  32. package/dist/converter.d.ts.map +1 -0
  33. package/dist/converter.js +836 -0
  34. package/dist/converter.js.map +1 -0
  35. package/dist/deconverter.d.ts +19 -0
  36. package/dist/deconverter.d.ts.map +1 -0
  37. package/dist/deconverter.js +188 -0
  38. package/dist/deconverter.js.map +1 -0
  39. package/dist/frameworks/angular-adapter.d.ts +27 -0
  40. package/dist/frameworks/angular-adapter.d.ts.map +1 -0
  41. package/dist/frameworks/angular-adapter.js +617 -0
  42. package/dist/frameworks/angular-adapter.js.map +1 -0
  43. package/dist/frameworks/index.d.ts +10 -0
  44. package/dist/frameworks/index.d.ts.map +1 -0
  45. package/dist/frameworks/index.js +21 -0
  46. package/dist/frameworks/index.js.map +1 -0
  47. package/dist/frameworks/nextjs-adapter.d.ts +22 -0
  48. package/dist/frameworks/nextjs-adapter.d.ts.map +1 -0
  49. package/dist/frameworks/nextjs-adapter.js +392 -0
  50. package/dist/frameworks/nextjs-adapter.js.map +1 -0
  51. package/dist/frameworks/react-adapter.d.ts +21 -0
  52. package/dist/frameworks/react-adapter.d.ts.map +1 -0
  53. package/dist/frameworks/react-adapter.js +71 -0
  54. package/dist/frameworks/react-adapter.js.map +1 -0
  55. package/dist/frameworks/svelte-adapter.d.ts +27 -0
  56. package/dist/frameworks/svelte-adapter.d.ts.map +1 -0
  57. package/dist/frameworks/svelte-adapter.js +519 -0
  58. package/dist/frameworks/svelte-adapter.js.map +1 -0
  59. package/dist/frameworks/types.d.ts +78 -0
  60. package/dist/frameworks/types.d.ts.map +1 -0
  61. package/dist/frameworks/types.js +2 -0
  62. package/dist/frameworks/types.js.map +1 -0
  63. package/dist/frameworks/vue-adapter.d.ts +34 -0
  64. package/dist/frameworks/vue-adapter.d.ts.map +1 -0
  65. package/dist/frameworks/vue-adapter.js +632 -0
  66. package/dist/frameworks/vue-adapter.js.map +1 -0
  67. package/dist/generators/accessibility-generator.d.ts +43 -0
  68. package/dist/generators/accessibility-generator.d.ts.map +1 -0
  69. package/dist/generators/accessibility-generator.js +507 -0
  70. package/dist/generators/accessibility-generator.js.map +1 -0
  71. package/dist/generators/asset-handler.d.ts +14 -0
  72. package/dist/generators/asset-handler.d.ts.map +1 -0
  73. package/dist/generators/asset-handler.js +79 -0
  74. package/dist/generators/asset-handler.js.map +1 -0
  75. package/dist/generators/build-verifier.d.ts +8 -0
  76. package/dist/generators/build-verifier.d.ts.map +1 -0
  77. package/dist/generators/build-verifier.js +64 -0
  78. package/dist/generators/build-verifier.js.map +1 -0
  79. package/dist/generators/component-extractor.d.ts +25 -0
  80. package/dist/generators/component-extractor.d.ts.map +1 -0
  81. package/dist/generators/component-extractor.js +146 -0
  82. package/dist/generators/component-extractor.js.map +1 -0
  83. package/dist/generators/component-generator.d.ts +12 -0
  84. package/dist/generators/component-generator.d.ts.map +1 -0
  85. package/dist/generators/component-generator.js +724 -0
  86. package/dist/generators/component-generator.js.map +1 -0
  87. package/dist/generators/deploy-generator.d.ts +9 -0
  88. package/dist/generators/deploy-generator.d.ts.map +1 -0
  89. package/dist/generators/deploy-generator.js +409 -0
  90. package/dist/generators/deploy-generator.js.map +1 -0
  91. package/dist/generators/error-boundary.d.ts +5 -0
  92. package/dist/generators/error-boundary.d.ts.map +1 -0
  93. package/dist/generators/error-boundary.js +59 -0
  94. package/dist/generators/error-boundary.js.map +1 -0
  95. package/dist/generators/form-generator.d.ts +42 -0
  96. package/dist/generators/form-generator.d.ts.map +1 -0
  97. package/dist/generators/form-generator.js +662 -0
  98. package/dist/generators/form-generator.js.map +1 -0
  99. package/dist/generators/hooks-generator.d.ts +40 -0
  100. package/dist/generators/hooks-generator.d.ts.map +1 -0
  101. package/dist/generators/hooks-generator.js +297 -0
  102. package/dist/generators/hooks-generator.js.map +1 -0
  103. package/dist/generators/html-generator.d.ts +27 -0
  104. package/dist/generators/html-generator.d.ts.map +1 -0
  105. package/dist/generators/html-generator.js +772 -0
  106. package/dist/generators/html-generator.js.map +1 -0
  107. package/dist/generators/jquery-converter.d.ts +41 -0
  108. package/dist/generators/jquery-converter.d.ts.map +1 -0
  109. package/dist/generators/jquery-converter.js +594 -0
  110. package/dist/generators/jquery-converter.js.map +1 -0
  111. package/dist/generators/pattern-implementer.d.ts +26 -0
  112. package/dist/generators/pattern-implementer.d.ts.map +1 -0
  113. package/dist/generators/pattern-implementer.js +336 -0
  114. package/dist/generators/pattern-implementer.js.map +1 -0
  115. package/dist/generators/performance-generator.d.ts +51 -0
  116. package/dist/generators/performance-generator.d.ts.map +1 -0
  117. package/dist/generators/performance-generator.js +428 -0
  118. package/dist/generators/performance-generator.js.map +1 -0
  119. package/dist/generators/router-generator.d.ts +21 -0
  120. package/dist/generators/router-generator.d.ts.map +1 -0
  121. package/dist/generators/router-generator.js +178 -0
  122. package/dist/generators/router-generator.js.map +1 -0
  123. package/dist/generators/scaffolder.d.ts +28 -0
  124. package/dist/generators/scaffolder.d.ts.map +1 -0
  125. package/dist/generators/scaffolder.js +266 -0
  126. package/dist/generators/scaffolder.js.map +1 -0
  127. package/dist/generators/seo-generator.d.ts +29 -0
  128. package/dist/generators/seo-generator.d.ts.map +1 -0
  129. package/dist/generators/seo-generator.js +223 -0
  130. package/dist/generators/seo-generator.js.map +1 -0
  131. package/dist/generators/test-generator.d.ts +19 -0
  132. package/dist/generators/test-generator.d.ts.map +1 -0
  133. package/dist/generators/test-generator.js +398 -0
  134. package/dist/generators/test-generator.js.map +1 -0
  135. package/dist/generators/type-generator.d.ts +33 -0
  136. package/dist/generators/type-generator.d.ts.map +1 -0
  137. package/dist/generators/type-generator.js +663 -0
  138. package/dist/generators/type-generator.js.map +1 -0
  139. package/dist/index.d.ts +23 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +12 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/parsers/css-processor.d.ts +23 -0
  144. package/dist/parsers/css-processor.d.ts.map +1 -0
  145. package/dist/parsers/css-processor.js +129 -0
  146. package/dist/parsers/css-processor.js.map +1 -0
  147. package/dist/parsers/framework-parser.d.ts +48 -0
  148. package/dist/parsers/framework-parser.d.ts.map +1 -0
  149. package/dist/parsers/framework-parser.js +770 -0
  150. package/dist/parsers/framework-parser.js.map +1 -0
  151. package/dist/parsers/html-parser.d.ts +12 -0
  152. package/dist/parsers/html-parser.d.ts.map +1 -0
  153. package/dist/parsers/html-parser.js +444 -0
  154. package/dist/parsers/html-parser.js.map +1 -0
  155. package/dist/parsers/js-analyzer.d.ts +199 -0
  156. package/dist/parsers/js-analyzer.d.ts.map +1 -0
  157. package/dist/parsers/js-analyzer.js +680 -0
  158. package/dist/parsers/js-analyzer.js.map +1 -0
  159. package/dist/parsers/js-resolver.d.ts +8 -0
  160. package/dist/parsers/js-resolver.d.ts.map +1 -0
  161. package/dist/parsers/js-resolver.js +45 -0
  162. package/dist/parsers/js-resolver.js.map +1 -0
  163. package/dist/parsers/tailwind-detector.d.ts +23 -0
  164. package/dist/parsers/tailwind-detector.d.ts.map +1 -0
  165. package/dist/parsers/tailwind-detector.js +104 -0
  166. package/dist/parsers/tailwind-detector.js.map +1 -0
  167. package/dist/tests/advanced-features.test.d.ts +2 -0
  168. package/dist/tests/advanced-features.test.d.ts.map +1 -0
  169. package/dist/tests/advanced-features.test.js +235 -0
  170. package/dist/tests/advanced-features.test.js.map +1 -0
  171. package/dist/tests/css-modules.test.d.ts +2 -0
  172. package/dist/tests/css-modules.test.d.ts.map +1 -0
  173. package/dist/tests/css-modules.test.js +61 -0
  174. package/dist/tests/css-modules.test.js.map +1 -0
  175. package/dist/tests/css-processor.test.d.ts +2 -0
  176. package/dist/tests/css-processor.test.d.ts.map +1 -0
  177. package/dist/tests/css-processor.test.js +48 -0
  178. package/dist/tests/css-processor.test.js.map +1 -0
  179. package/dist/tests/html-parser.test.d.ts +2 -0
  180. package/dist/tests/html-parser.test.d.ts.map +1 -0
  181. package/dist/tests/html-parser.test.js +78 -0
  182. package/dist/tests/html-parser.test.js.map +1 -0
  183. package/dist/tests/integration.test.d.ts +2 -0
  184. package/dist/tests/integration.test.d.ts.map +1 -0
  185. package/dist/tests/integration.test.js +65 -0
  186. package/dist/tests/integration.test.js.map +1 -0
  187. package/dist/tests/js-analyzer.test.d.ts +2 -0
  188. package/dist/tests/js-analyzer.test.d.ts.map +1 -0
  189. package/dist/tests/js-analyzer.test.js +58 -0
  190. package/dist/tests/js-analyzer.test.js.map +1 -0
  191. package/dist/tests/naming.test.d.ts +2 -0
  192. package/dist/tests/naming.test.d.ts.map +1 -0
  193. package/dist/tests/naming.test.js +43 -0
  194. package/dist/tests/naming.test.js.map +1 -0
  195. package/dist/tests/router-generator.test.d.ts +2 -0
  196. package/dist/tests/router-generator.test.d.ts.map +1 -0
  197. package/dist/tests/router-generator.test.js +60 -0
  198. package/dist/tests/router-generator.test.js.map +1 -0
  199. package/dist/tui/chat.d.ts +13 -0
  200. package/dist/tui/chat.d.ts.map +1 -0
  201. package/dist/tui/chat.js +499 -0
  202. package/dist/tui/chat.js.map +1 -0
  203. package/dist/tui/design-guide.d.ts +41 -0
  204. package/dist/tui/design-guide.d.ts.map +1 -0
  205. package/dist/tui/design-guide.js +184 -0
  206. package/dist/tui/design-guide.js.map +1 -0
  207. package/dist/tui/input.d.ts +30 -0
  208. package/dist/tui/input.d.ts.map +1 -0
  209. package/dist/tui/input.js +239 -0
  210. package/dist/tui/input.js.map +1 -0
  211. package/dist/tui/renderer.d.ts +48 -0
  212. package/dist/tui/renderer.d.ts.map +1 -0
  213. package/dist/tui/renderer.js +212 -0
  214. package/dist/tui/renderer.js.map +1 -0
  215. package/dist/tui/tools.d.ts +14 -0
  216. package/dist/tui/tools.d.ts.map +1 -0
  217. package/dist/tui/tools.js +1370 -0
  218. package/dist/tui/tools.js.map +1 -0
  219. package/dist/types.d.ts +93 -0
  220. package/dist/types.d.ts.map +1 -0
  221. package/dist/types.js +2 -0
  222. package/dist/types.js.map +1 -0
  223. package/dist/utils/config.d.ts +20 -0
  224. package/dist/utils/config.d.ts.map +1 -0
  225. package/dist/utils/config.js +33 -0
  226. package/dist/utils/config.js.map +1 -0
  227. package/dist/utils/formatter.d.ts +5 -0
  228. package/dist/utils/formatter.d.ts.map +1 -0
  229. package/dist/utils/formatter.js +68 -0
  230. package/dist/utils/formatter.js.map +1 -0
  231. package/dist/utils/logger.d.ts +8 -0
  232. package/dist/utils/logger.d.ts.map +1 -0
  233. package/dist/utils/logger.js +19 -0
  234. package/dist/utils/logger.js.map +1 -0
  235. package/dist/utils/naming.d.ts +17 -0
  236. package/dist/utils/naming.d.ts.map +1 -0
  237. package/dist/utils/naming.js +48 -0
  238. package/dist/utils/naming.js.map +1 -0
  239. package/dist/utils/report.d.ts +56 -0
  240. package/dist/utils/report.d.ts.map +1 -0
  241. package/dist/utils/report.js +339 -0
  242. package/dist/utils/report.js.map +1 -0
  243. package/package.json +61 -0
@@ -0,0 +1,836 @@
1
+ import { readdir, readFile } from 'fs/promises';
2
+ import { writeFile, mkdir } from 'fs/promises';
3
+ import { join, extname, resolve, basename, dirname } from 'path';
4
+ import { getFrameworkAdapter } from './frameworks/index.js';
5
+ import { parseHTMLFile } from './parsers/html-parser.js';
6
+ import { processCSSFile, mergeCSS, rewriteCSSUrls } from './parsers/css-processor.js';
7
+ import { resolveAndAnalyzeJS } from './parsers/js-resolver.js';
8
+ import { generateComponent, renderComponentFile } from './generators/component-generator.js';
9
+ import { scaffoldProject } from './generators/scaffolder.js';
10
+ import { discoverAssets, copyAssets } from './generators/asset-handler.js';
11
+ import { verifyBuild } from './generators/build-verifier.js';
12
+ import { generateRoutes, generateRouterApp, generateSimpleApp } from './generators/router-generator.js';
13
+ import { extractComponents, detectSharedLayout } from './generators/component-extractor.js';
14
+ import { generateErrorBoundary } from './generators/error-boundary.js';
15
+ import { generateUseFetchHook, generateUseStorageHook, generateUseIntervalHook, generateUseTimeoutHook, generateUseScrollRevealHook, generateUseScrollClassHook, generateUseCounterAnimationHook, determineNeededHooks, } from './generators/hooks-generator.js';
16
+ import { detectTailwind, generateTailwindConfig, generatePostCSSConfig, generateTailwindCSS } from './parsers/tailwind-detector.js';
17
+ import { loadConfig } from './utils/config.js';
18
+ import { formatProject } from './utils/formatter.js';
19
+ import { generateReport } from './utils/report.js';
20
+ import { toKebabCase, toComponentName } from './utils/naming.js';
21
+ import { logger } from './utils/logger.js';
22
+ /**
23
+ * Main conversion pipeline: HTML/CSS/JS site -> modern framework project.
24
+ * Supports React (Vite), Next.js, Vue, Svelte, and Angular.
25
+ */
26
+ export async function convert(options) {
27
+ const startTime = Date.now();
28
+ const inputDir = resolve(options.input);
29
+ const outputDir = resolve(options.output);
30
+ // Load config (file + CLI overrides)
31
+ const config = await loadConfig(inputDir, options);
32
+ const cssModules = config.cssModules ?? options.cssModules ?? false;
33
+ const shouldExtract = config.extractComponents ?? options.extractComponents ?? true;
34
+ const shouldGenerateHooks = config.generateHooks ?? true;
35
+ const shouldErrorBoundary = config.errorBoundary ?? true;
36
+ const shouldFormat = config.prettier ?? options.prettier ?? true;
37
+ const tailwindMode = config.tailwind ?? 'auto';
38
+ const totalSteps = 10;
39
+ let step = 0;
40
+ const result = {
41
+ outputDir,
42
+ components: [],
43
+ assets: [],
44
+ buildSuccess: false,
45
+ errors: [],
46
+ warnings: [],
47
+ };
48
+ // === STEP 1: Discover HTML files ===
49
+ step++;
50
+ logger.step(step, totalSteps, 'Discovering source files...');
51
+ const htmlFiles = await discoverHTMLFiles(inputDir);
52
+ if (htmlFiles.length === 0) {
53
+ result.errors.push('No HTML files found in input directory');
54
+ return result;
55
+ }
56
+ logger.info(`Found ${htmlFiles.length} HTML file(s)`);
57
+ const isMultiPage = htmlFiles.length > 1;
58
+ if (isMultiPage)
59
+ logger.info('Multi-page site detected — generating React Router setup');
60
+ // === STEP 2: Parse HTML files ===
61
+ step++;
62
+ logger.step(step, totalSteps, 'Parsing HTML files...');
63
+ const parsedFiles = [];
64
+ for (const file of htmlFiles) {
65
+ try {
66
+ const parsed = await parseHTMLFile(file);
67
+ parsedFiles.push(parsed);
68
+ }
69
+ catch (err) {
70
+ const msg = err instanceof Error ? err.message : String(err);
71
+ result.warnings.push(`Failed to parse ${file}: ${msg}`);
72
+ }
73
+ }
74
+ // === STEP 3: Detect Tailwind CSS ===
75
+ step++;
76
+ logger.step(step, totalSteps, 'Analyzing CSS framework...');
77
+ let tailwindDetection = null;
78
+ if (tailwindMode !== 'disable') {
79
+ for (const parsed of parsedFiles) {
80
+ const fullHtml = parsed.headContent + parsed.bodyContent;
81
+ const detection = detectTailwind(fullHtml);
82
+ if (detection.isTailwind || tailwindMode === 'force') {
83
+ tailwindDetection = detection;
84
+ logger.info(`Tailwind CSS detected (confidence: ${Math.round(detection.confidence * 100)}%)`);
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ // === STEP 4: Analyze JavaScript (including linked files) ===
90
+ step++;
91
+ logger.step(step, totalSteps, 'Analyzing JavaScript patterns...');
92
+ const analyses = [];
93
+ for (const parsed of parsedFiles) {
94
+ const analysis = await resolveAndAnalyzeJS(parsed, inputDir);
95
+ analyses.push(analysis);
96
+ // Log detected patterns
97
+ const patterns = [];
98
+ if (analysis.stateVars.length > 0)
99
+ patterns.push(`${analysis.stateVars.length} state var(s)`);
100
+ if (analysis.eventHandlers.length > 0)
101
+ patterns.push(`${analysis.eventHandlers.length} handler(s)`);
102
+ if (analysis.fetchCalls.length > 0)
103
+ patterns.push(`${analysis.fetchCalls.length} fetch call(s)`);
104
+ if (analysis.storageCalls.length > 0)
105
+ patterns.push(`${analysis.storageCalls.length} storage call(s)`);
106
+ if (analysis.jqueryPatterns.length > 0)
107
+ patterns.push(`${analysis.jqueryPatterns.length} jQuery pattern(s)`);
108
+ if (analysis.domMutations.length > 0)
109
+ patterns.push(`${analysis.domMutations.length} DOM mutation(s)`);
110
+ if (analysis.timerCalls.length > 0)
111
+ patterns.push(`${analysis.timerCalls.length} timer(s)`);
112
+ if (patterns.length > 0) {
113
+ logger.info(` ${parsed.fileName}: ${patterns.join(', ')}`);
114
+ }
115
+ }
116
+ // Detect hash-based SPA routing (single HTML file with multiple virtual pages)
117
+ const hasHashRouting = analyses.some(a => a.interactivePatterns?.historyRouting?.some(hr => hr.type === 'hash') ||
118
+ a.pageTemplates.length > 1);
119
+ const needsRouter = isMultiPage || hasHashRouting;
120
+ if (hasHashRouting && !isMultiPage)
121
+ logger.info('Hash-based SPA routing detected — generating React Router setup');
122
+ // === STEP 5: Scaffold project ===
123
+ step++;
124
+ const framework = options.framework || 'react';
125
+ const adapter = getFrameworkAdapter(framework);
126
+ logger.step(step, totalSteps, `Scaffolding ${adapter.name} project...`);
127
+ const projectName = options.componentName || basename(inputDir);
128
+ // Collect meta, external resources, favicons from first (main) parsed file
129
+ const mainParsed = parsedFiles[0];
130
+ const allExternalStyles = [...new Set(parsedFiles.flatMap(p => p.externalStyles || []))];
131
+ const allExternalScripts = [...new Set(parsedFiles.flatMap(p => p.externalScripts || []))];
132
+ const allPreconnectLinks = [...new Set(parsedFiles.flatMap(p => p.preconnectLinks || []))];
133
+ const allStructuredData = parsedFiles.flatMap(p => p.structuredData || []);
134
+ if (framework === 'react') {
135
+ await scaffoldProject(outputDir, projectName, needsRouter, !!tailwindDetection, {
136
+ title: mainParsed?.title,
137
+ meta: mainParsed?.meta,
138
+ externalStyles: allExternalStyles,
139
+ externalScripts: allExternalScripts,
140
+ preconnectLinks: allPreconnectLinks,
141
+ structuredData: allStructuredData,
142
+ favicons: mainParsed?.favicons,
143
+ manifest: mainParsed?.manifest,
144
+ });
145
+ }
146
+ else {
147
+ await adapter.scaffoldProject(outputDir, projectName, {
148
+ useRouter: needsRouter,
149
+ useTailwind: !!tailwindDetection,
150
+ title: mainParsed?.title,
151
+ meta: mainParsed?.meta,
152
+ externalStyles: allExternalStyles,
153
+ externalScripts: allExternalScripts,
154
+ preconnectLinks: allPreconnectLinks,
155
+ structuredData: allStructuredData,
156
+ favicons: mainParsed?.favicons,
157
+ manifest: mainParsed?.manifest,
158
+ });
159
+ }
160
+ if (allExternalStyles.length > 0) {
161
+ logger.info(`Preserved ${allExternalStyles.length} external stylesheet(s) (CDN fonts/icons)`);
162
+ }
163
+ if (allExternalScripts.length > 0) {
164
+ logger.info(`Preserved ${allExternalScripts.length} external script(s)`);
165
+ }
166
+ // Tailwind config files
167
+ if (tailwindDetection && framework === 'react') {
168
+ await Promise.all([
169
+ writeFile(join(outputDir, 'tailwind.config.js'), generateTailwindConfig()),
170
+ writeFile(join(outputDir, 'postcss.config.js'), generatePostCSSConfig()),
171
+ writeFile(join(outputDir, 'src', 'styles', 'tailwind.css'), generateTailwindCSS()),
172
+ ]);
173
+ }
174
+ // Error boundary (React-specific; other frameworks handle it differently)
175
+ if (shouldErrorBoundary && framework === 'react') {
176
+ await writeFile(join(outputDir, 'src', 'components', 'ErrorBoundary.tsx'), generateErrorBoundary());
177
+ }
178
+ // === STEP 6: Generate custom hooks ===
179
+ step++;
180
+ logger.step(step, totalSteps, 'Generating custom hooks...');
181
+ // Merge all interactive patterns across pages
182
+ const mergedInteractive = {
183
+ scrollReveals: analyses.flatMap(a => a.interactivePatterns?.scrollReveals || []),
184
+ tabs: analyses.flatMap(a => a.interactivePatterns?.tabs || []),
185
+ accordions: analyses.flatMap(a => a.interactivePatterns?.accordions || []),
186
+ scrollClasses: analyses.flatMap(a => a.interactivePatterns?.scrollClasses || []),
187
+ counterAnimations: analyses.flatMap(a => a.interactivePatterns?.counterAnimations || []),
188
+ carousels: analyses.flatMap(a => a.interactivePatterns?.carousels || []),
189
+ eventDelegations: analyses.flatMap(a => a.interactivePatterns?.eventDelegations || []),
190
+ debounces: analyses.flatMap(a => a.interactivePatterns?.debounces || []),
191
+ formValidations: analyses.flatMap(a => a.interactivePatterns?.formValidations || []),
192
+ historyRouting: analyses.flatMap(a => a.interactivePatterns?.historyRouting || []),
193
+ mobileMenus: analyses.flatMap(a => a.interactivePatterns?.mobileMenus || []),
194
+ formHandlers: analyses.flatMap(a => a.interactivePatterns?.formHandlers || []),
195
+ ripples: analyses.flatMap(a => a.interactivePatterns?.ripples || []),
196
+ };
197
+ const allAnalysis = {
198
+ fetchCalls: analyses.flatMap(a => a.fetchCalls),
199
+ storageCalls: analyses.flatMap(a => a.storageCalls),
200
+ timerCalls: analyses.flatMap(a => a.timerCalls),
201
+ interactivePatterns: mergedInteractive,
202
+ };
203
+ const neededHooks = shouldGenerateHooks ? determineNeededHooks(allAnalysis) : [];
204
+ if (neededHooks.length > 0) {
205
+ if (framework === 'react') {
206
+ const hooksDir = join(outputDir, 'src', 'hooks');
207
+ await mkdir(hooksDir, { recursive: true });
208
+ for (const hook of neededHooks) {
209
+ let content = '';
210
+ switch (hook) {
211
+ case 'useFetch':
212
+ content = generateUseFetchHook();
213
+ break;
214
+ case 'useLocalStorage':
215
+ content = generateUseStorageHook('localStorage');
216
+ break;
217
+ case 'useSessionStorage':
218
+ content = generateUseStorageHook('sessionStorage');
219
+ break;
220
+ case 'useInterval':
221
+ content = generateUseIntervalHook();
222
+ break;
223
+ case 'useTimeout':
224
+ content = generateUseTimeoutHook();
225
+ break;
226
+ case 'useScrollReveal': {
227
+ const threshold = mergedInteractive.scrollReveals[0]?.threshold ?? 0.1;
228
+ content = generateUseScrollRevealHook(threshold);
229
+ break;
230
+ }
231
+ case 'useScrollClass':
232
+ content = generateUseScrollClassHook();
233
+ break;
234
+ case 'useCounterAnimation':
235
+ content = generateUseCounterAnimationHook();
236
+ break;
237
+ }
238
+ if (content) {
239
+ await writeFile(join(hooksDir, `${hook}.ts`), content);
240
+ }
241
+ }
242
+ }
243
+ else {
244
+ // Use the framework adapter to generate hooks/composables
245
+ const hookFiles = adapter.generateHooks(neededHooks, mergedInteractive);
246
+ for (const [relPath, content] of hookFiles) {
247
+ const fullPath = join(outputDir, 'src', relPath);
248
+ await mkdir(dirname(fullPath), { recursive: true });
249
+ await writeFile(fullPath, content);
250
+ }
251
+ }
252
+ logger.info(`Generated ${neededHooks.length} custom hook(s): ${neededHooks.join(', ')}`);
253
+ }
254
+ // Log detected interactive patterns
255
+ const patternNames = [];
256
+ if (mergedInteractive.scrollReveals.length > 0)
257
+ patternNames.push(`${mergedInteractive.scrollReveals.length} scroll-reveal(s)`);
258
+ if (mergedInteractive.tabs.length > 0)
259
+ patternNames.push(`${mergedInteractive.tabs.length} tab set(s)`);
260
+ if (mergedInteractive.accordions.length > 0)
261
+ patternNames.push(`${mergedInteractive.accordions.length} accordion(s)`);
262
+ if (mergedInteractive.scrollClasses.length > 0)
263
+ patternNames.push(`${mergedInteractive.scrollClasses.length} scroll-class toggle(s)`);
264
+ if (mergedInteractive.counterAnimations.length > 0)
265
+ patternNames.push(`${mergedInteractive.counterAnimations.length} counter animation(s)`);
266
+ if (mergedInteractive.mobileMenus.length > 0)
267
+ patternNames.push(`${mergedInteractive.mobileMenus.length} mobile menu(s)`);
268
+ if (mergedInteractive.formHandlers.length > 0)
269
+ patternNames.push(`${mergedInteractive.formHandlers.length} form handler(s)`);
270
+ if (mergedInteractive.ripples.length > 0)
271
+ patternNames.push(`${mergedInteractive.ripples.length} ripple effect(s)`);
272
+ if (patternNames.length > 0) {
273
+ logger.info(`Detected interactive patterns: ${patternNames.join(', ')}`);
274
+ }
275
+ // === STEP 7: Process CSS files ===
276
+ step++;
277
+ logger.step(step, totalSteps, 'Processing stylesheets...');
278
+ const globalCSSParts = [];
279
+ const processedCSSPaths = new Set();
280
+ for (const parsed of parsedFiles) {
281
+ for (const cssPath of parsed.linkedStyles) {
282
+ const htmlDir = parsed.filePath ? dirname(parsed.filePath) : inputDir;
283
+ const fullCssPath = resolve(htmlDir, cssPath);
284
+ if (processedCSSPaths.has(fullCssPath))
285
+ continue;
286
+ processedCSSPaths.add(fullCssPath);
287
+ try {
288
+ const processed = await processCSSFile(fullCssPath, cssModules);
289
+ const rewritten = rewriteCSSUrls(cssModules ? processed.moduleCSS : processed.globalCSS, (url) => url.startsWith('/') ? url : `/src/assets/${url}`);
290
+ globalCSSParts.push(rewritten);
291
+ if (processed.globalCSS && cssModules) {
292
+ globalCSSParts.push(processed.globalCSS);
293
+ }
294
+ }
295
+ catch (err) {
296
+ const msg = err instanceof Error ? err.message : String(err);
297
+ result.warnings.push(`Failed to process CSS ${cssPath}: ${msg}`);
298
+ }
299
+ }
300
+ }
301
+ if (globalCSSParts.length > 0) {
302
+ const globalCSS = mergeCSS(globalCSSParts);
303
+ const stylesDir = framework === 'svelte'
304
+ ? join(outputDir, 'src', 'lib', 'styles')
305
+ : framework === 'nextjs'
306
+ ? join(outputDir, 'app', 'styles')
307
+ : join(outputDir, 'src', 'styles');
308
+ await mkdir(stylesDir, { recursive: true });
309
+ await writeFile(join(stylesDir, 'global.css'), globalCSS);
310
+ }
311
+ // === STEP 8: Generate components ===
312
+ step++;
313
+ logger.step(step, totalSteps, `Generating ${adapter.name} components...`);
314
+ const componentNames = [];
315
+ const fileNames = [];
316
+ let extractedComponentCount = 0;
317
+ // Detect shared layout (header/footer) across pages
318
+ if (isMultiPage && shouldExtract) {
319
+ const bodyContents = parsedFiles.map(p => p.bodyContent);
320
+ const shared = detectSharedLayout(bodyContents);
321
+ if (shared.header) {
322
+ logger.info('Shared header detected — extracting Layout component');
323
+ }
324
+ if (shared.footer) {
325
+ logger.info('Shared footer detected — extracting Layout component');
326
+ }
327
+ }
328
+ // Determine component directory based on framework
329
+ const componentDir = framework === 'svelte'
330
+ ? join(outputDir, 'src', 'lib', 'components')
331
+ : framework === 'angular'
332
+ ? join(outputDir, 'src', 'app', 'components')
333
+ : framework === 'nextjs'
334
+ ? join(outputDir, 'app', 'components')
335
+ : join(outputDir, 'src', 'components');
336
+ await mkdir(componentDir, { recursive: true });
337
+ for (let i = 0; i < parsedFiles.length; i++) {
338
+ const parsed = parsedFiles[i];
339
+ const analysis = analyses[i];
340
+ // Extract sub-components if enabled
341
+ const subComponentImports = [];
342
+ if (shouldExtract) {
343
+ const { sections, parentHtml } = extractComponents(parsed.bodyContent);
344
+ if (sections.length > 0) {
345
+ extractedComponentCount += sections.length;
346
+ for (const section of sections) {
347
+ const subParsed = {
348
+ ...parsed,
349
+ fileName: `${section.name}.html`,
350
+ bodyContent: section.html,
351
+ inlineScripts: [],
352
+ linkedScripts: [],
353
+ inlineStyles: [],
354
+ };
355
+ const subComponent = generateComponent(subParsed, cssModules);
356
+ // Pass a lightweight analysis to sub-components
357
+ const subAnalysis = {
358
+ ...analysis,
359
+ refs: [],
360
+ stateVars: [],
361
+ effects: [],
362
+ eventHandlers: [],
363
+ domMutations: [],
364
+ timerCalls: [],
365
+ fetchCalls: [],
366
+ storageCalls: [],
367
+ jqueryPatterns: [],
368
+ utilityCode: [],
369
+ };
370
+ if (framework === 'react') {
371
+ const subCode = renderComponentFile(subComponent, cssModules, neededHooks, subAnalysis);
372
+ const subFileName = `${toKebabCase(section.name)}.tsx`;
373
+ await writeFile(join(componentDir, subFileName), subCode);
374
+ }
375
+ else {
376
+ const subCode = adapter.renderComponent(subComponent, {
377
+ cssModules,
378
+ customHooks: neededHooks,
379
+ analysis: subAnalysis,
380
+ });
381
+ const subFileName = `${toKebabCase(section.name)}${adapter.componentExtension}`;
382
+ // For Angular, create subdirectory per component
383
+ if (framework === 'angular') {
384
+ const subDir = join(componentDir, toKebabCase(section.name));
385
+ await mkdir(subDir, { recursive: true });
386
+ await writeFile(join(subDir, `${toKebabCase(section.name)}.component${adapter.componentExtension}`), subCode);
387
+ }
388
+ else {
389
+ await writeFile(join(componentDir, subFileName), subCode);
390
+ }
391
+ }
392
+ subComponentImports.push(section.name);
393
+ }
394
+ parsed.bodyContent = parentHtml;
395
+ }
396
+ }
397
+ // If page templates exist, inject home page content into the main element
398
+ if (analysis.pageTemplates.length > 0) {
399
+ const homeTemplate = analysis.pageTemplates.find(t => t.route === '/');
400
+ if (homeTemplate) {
401
+ const targetId = homeTemplate.targetElement;
402
+ const emptyPattern = new RegExp(`(<[^>]*id=["']${targetId}["'][^>]*>)\\s*</`, 'i');
403
+ if (emptyPattern.test(parsed.bodyContent)) {
404
+ parsed.bodyContent = parsed.bodyContent.replace(emptyPattern, `$1${homeTemplate.htmlContent}</`);
405
+ }
406
+ }
407
+ // Generate separate components for each page template (non-home)
408
+ for (const pt of analysis.pageTemplates) {
409
+ if (pt.route === '/')
410
+ continue;
411
+ const pageName = pt.functionName.replace(/^render/, '').replace(/Page$/, '');
412
+ const pageParsed = {
413
+ ...parsed,
414
+ fileName: `${pageName}.html`,
415
+ bodyContent: pt.htmlContent,
416
+ inlineScripts: [],
417
+ linkedScripts: [],
418
+ inlineStyles: [],
419
+ };
420
+ const pageComponent = generateComponent(pageParsed, cssModules);
421
+ const pageFileName = toKebabCase(pageComponent.name);
422
+ if (framework === 'react') {
423
+ const pageCode = renderComponentFile(pageComponent, cssModules);
424
+ await writeFile(join(componentDir, `${pageFileName}.tsx`), pageCode);
425
+ }
426
+ else {
427
+ const pageCode = adapter.renderComponent(pageComponent, { cssModules, customHooks: neededHooks });
428
+ await writeFile(join(componentDir, `${pageFileName}${adapter.componentExtension}`), pageCode);
429
+ }
430
+ extractedComponentCount++;
431
+ }
432
+ }
433
+ const component = generateComponent(parsed, cssModules, analysis);
434
+ if (framework === 'react') {
435
+ const componentCode = renderComponentFile(component, cssModules, neededHooks, analysis, subComponentImports);
436
+ const componentFileName = `${toKebabCase(component.name)}.tsx`;
437
+ await writeFile(join(componentDir, componentFileName), componentCode);
438
+ }
439
+ else {
440
+ const componentCode = adapter.renderComponent(component, {
441
+ cssModules,
442
+ customHooks: neededHooks,
443
+ analysis,
444
+ subComponentImports,
445
+ isPage: true,
446
+ });
447
+ if (framework === 'angular') {
448
+ const angDir = join(componentDir, toKebabCase(component.name));
449
+ await mkdir(angDir, { recursive: true });
450
+ await writeFile(join(angDir, `${toKebabCase(component.name)}.component${adapter.componentExtension}`), componentCode);
451
+ }
452
+ else {
453
+ const componentFileName = `${toKebabCase(component.name)}${adapter.componentExtension}`;
454
+ await writeFile(join(componentDir, componentFileName), componentCode);
455
+ }
456
+ }
457
+ // Write CSS file for non-Vue/Svelte (they embed CSS in SFC)
458
+ if (component.css && framework === 'react') {
459
+ await writeFile(join(componentDir, component.cssFileName), component.css);
460
+ }
461
+ else if (component.css && (framework === 'nextjs' || framework === 'angular')) {
462
+ await writeFile(join(componentDir, component.cssFileName), component.css);
463
+ }
464
+ result.components.push(component.name);
465
+ componentNames.push(component.name);
466
+ fileNames.push(parsed.fileName);
467
+ }
468
+ // Generate App entry / routing
469
+ const hasGlobalCSS = globalCSSParts.length > 0;
470
+ let routeCount = 0;
471
+ // Build hash-based SPA route info (shared across all frameworks)
472
+ let hashRouteNames = null;
473
+ let hashRouteFiles = null;
474
+ let hashRoutePaths = null;
475
+ if (hasHashRouting && !isMultiPage) {
476
+ const allPageTemplates = analyses.flatMap(a => a.pageTemplates);
477
+ const pageRouteNames = [];
478
+ const pageRouteFiles = [];
479
+ const seenRoutes = new Set();
480
+ // Add the main page component (shell with nav/footer)
481
+ pageRouteNames.push(componentNames[0]);
482
+ pageRouteFiles.push('index.html');
483
+ seenRoutes.add('/');
484
+ // Add page template routes (deduplicated)
485
+ for (const pt of allPageTemplates) {
486
+ const route = pt.route || `/${pt.functionName.replace(/^render/, '').replace(/Page$/, '').toLowerCase()}`;
487
+ if (route === '/')
488
+ continue;
489
+ if (seenRoutes.has(route))
490
+ continue;
491
+ seenRoutes.add(route);
492
+ const pageName = pt.functionName.replace(/^render/, '').replace(/Page$/, '');
493
+ const componentName = toComponentName(`${pageName}.html`);
494
+ pageRouteNames.push(componentName);
495
+ pageRouteFiles.push(`${pageName.toLowerCase()}.html`);
496
+ }
497
+ hashRouteNames = pageRouteNames;
498
+ hashRouteFiles = pageRouteFiles;
499
+ hashRoutePaths = Array.from(seenRoutes);
500
+ }
501
+ if (framework === 'react') {
502
+ let appContent;
503
+ if (hashRouteNames && hashRouteFiles && hashRoutePaths) {
504
+ const routes = generateRoutes(hashRouteNames, hashRouteFiles);
505
+ // Override route paths with actual hash routes
506
+ for (let i = 0; i < routes.length && i < hashRoutePaths.length; i++) {
507
+ routes[i].path = hashRoutePaths[i];
508
+ }
509
+ routeCount = routes.length;
510
+ appContent = generateRouterApp(routes, hasGlobalCSS, shouldErrorBoundary, !!tailwindDetection, true);
511
+ logger.info(`Generated ${routes.length} hash route(s) using HashRouter`);
512
+ }
513
+ else if (isMultiPage) {
514
+ const routes = generateRoutes(componentNames, fileNames);
515
+ routeCount = routes.length;
516
+ appContent = generateRouterApp(routes, hasGlobalCSS, shouldErrorBoundary, !!tailwindDetection);
517
+ logger.info(`Generated ${routes.length} route(s)`);
518
+ }
519
+ else {
520
+ routeCount = 1;
521
+ appContent = generateSimpleApp(componentNames, hasGlobalCSS, shouldErrorBoundary, !!tailwindDetection);
522
+ }
523
+ await writeFile(join(outputDir, 'src', 'App.tsx'), appContent);
524
+ }
525
+ else {
526
+ // Use framework adapter for app entry & routing
527
+ const useHashRouting = !!(hashRouteNames && hashRouteFiles && hashRoutePaths);
528
+ const routingComponentNames = useHashRouting ? hashRouteNames : componentNames;
529
+ const routingFileNames = useHashRouting ? hashRouteFiles : fileNames;
530
+ if (needsRouter) {
531
+ const routeFiles = adapter.generateRouting({
532
+ componentNames: routingComponentNames,
533
+ fileNames: routingFileNames,
534
+ hasGlobalCSS,
535
+ errorBoundary: shouldErrorBoundary,
536
+ tailwind: !!tailwindDetection,
537
+ routePaths: hashRoutePaths || undefined,
538
+ });
539
+ routeCount = routingComponentNames.length;
540
+ for (const [relPath, content] of routeFiles) {
541
+ const fullPath = join(outputDir, framework === 'nextjs' ? '' : 'src', relPath);
542
+ await mkdir(dirname(fullPath), { recursive: true });
543
+ await writeFile(fullPath, content);
544
+ }
545
+ logger.info(`Generated ${routeCount} route(s)${useHashRouting ? ' from hash-based SPA' : ''}`);
546
+ }
547
+ else {
548
+ routeCount = 1;
549
+ }
550
+ // App entry file
551
+ const appEntry = adapter.generateAppEntry({
552
+ componentNames: routingComponentNames,
553
+ hasGlobalCSS,
554
+ errorBoundary: shouldErrorBoundary,
555
+ tailwind: !!tailwindDetection,
556
+ isMultiPage: needsRouter,
557
+ });
558
+ const appEntryPath = framework === 'nextjs'
559
+ ? join(outputDir, 'app', 'layout.tsx')
560
+ : framework === 'svelte'
561
+ ? join(outputDir, 'src', 'routes', '+layout.svelte')
562
+ : framework === 'angular'
563
+ ? join(outputDir, 'src', 'app', 'app.component.ts')
564
+ : join(outputDir, 'src', 'App.vue');
565
+ await mkdir(dirname(appEntryPath), { recursive: true });
566
+ await writeFile(appEntryPath, appEntry);
567
+ // For Next.js single-page: generate page.tsx
568
+ if (framework === 'nextjs' && !needsRouter && componentNames.length > 0) {
569
+ const name = componentNames[0];
570
+ const kebab = toKebabCase(name);
571
+ const pageContent = `import ${name} from './components/${kebab}';\n\nexport default function Page() {\n return <${name} />;\n}\n`;
572
+ await writeFile(join(outputDir, 'app', 'page.tsx'), pageContent);
573
+ }
574
+ // For Svelte single-page: generate +page.svelte
575
+ if (framework === 'svelte' && !needsRouter && componentNames.length > 0) {
576
+ const name = componentNames[0];
577
+ const kebab = toKebabCase(name);
578
+ const pageContent = `<script lang="ts">\n import ${name} from '$lib/components/${kebab}.svelte';\n</script>\n\n<${name} />\n`;
579
+ await writeFile(join(outputDir, 'src', 'routes', '+page.svelte'), pageContent);
580
+ }
581
+ }
582
+ // === STEP 9: Copy assets ===
583
+ step++;
584
+ logger.step(step, totalSteps, 'Copying assets...');
585
+ const assets = await discoverAssets(inputDir);
586
+ const copiedAssets = await copyAssets(assets, outputDir);
587
+ result.assets = copiedAssets;
588
+ if (copiedAssets.length > 0) {
589
+ logger.info(`Copied ${copiedAssets.length} asset(s)`);
590
+ }
591
+ // === STEP 10: Format & Verify ===
592
+ step++;
593
+ logger.step(step, totalSteps, 'Formatting & verifying...');
594
+ let formattedCount = 0;
595
+ if (shouldFormat) {
596
+ try {
597
+ formattedCount = await formatProject(outputDir);
598
+ logger.info(`Formatted ${formattedCount} file(s) with Prettier`);
599
+ }
600
+ catch {
601
+ result.warnings.push('Prettier formatting failed — output is still valid');
602
+ }
603
+ }
604
+ if (options.verify) {
605
+ logger.info('Running build verification...');
606
+ const buildResult = await verifyBuild(outputDir);
607
+ result.buildSuccess = buildResult.success;
608
+ if (!buildResult.success) {
609
+ result.errors.push(...buildResult.errors);
610
+ }
611
+ }
612
+ // === Generate test files ===
613
+ if (options.generateTests && framework === 'react') {
614
+ const { generateVitestConfig, generateTestSetup, generateComponentTest, generateTestPackageDeps } = await import('./generators/test-generator.js');
615
+ const testDir = join(outputDir, 'src', 'test');
616
+ await mkdir(testDir, { recursive: true });
617
+ await writeFile(join(outputDir, 'vitest.config.ts'), generateVitestConfig());
618
+ await writeFile(join(testDir, 'setup.ts'), generateTestSetup());
619
+ // Generate test for each component
620
+ const componentTestDir = join(outputDir, 'src', 'components');
621
+ for (let i = 0; i < parsedFiles.length; i++) {
622
+ const comp = generateComponent(parsedFiles[i], cssModules, analyses[i]);
623
+ const testCode = generateComponentTest(comp, analyses[i]);
624
+ await writeFile(join(componentTestDir, `${toKebabCase(comp.name)}.test.tsx`), testCode);
625
+ }
626
+ // Add test deps to package.json
627
+ const pkgPath = join(outputDir, 'package.json');
628
+ try {
629
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
630
+ const testDeps = generateTestPackageDeps();
631
+ pkg.devDependencies = { ...pkg.devDependencies, ...testDeps };
632
+ pkg.scripts = { ...pkg.scripts, test: 'vitest', 'test:ui': 'vitest --ui', 'test:coverage': 'vitest --coverage' };
633
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
634
+ }
635
+ catch { /* skip if package.json can't be read */ }
636
+ logger.info('Generated Vitest test files');
637
+ }
638
+ // === Generate deployment configs ===
639
+ if (options.generateDeploy) {
640
+ const { generateDockerfile, generateDockerCompose, generateGithubActionsCI, generateVercelConfig, generateNetlifyConfig, generateEnvExample, generateNginxConf } = await import('./generators/deploy-generator.js');
641
+ const allFetchCalls = analyses.flatMap(a => a.fetchCalls);
642
+ await writeFile(join(outputDir, 'Dockerfile'), generateDockerfile(framework));
643
+ await writeFile(join(outputDir, 'docker-compose.yml'), generateDockerCompose(projectName));
644
+ await writeFile(join(outputDir, 'nginx.conf'), generateNginxConf(isMultiPage));
645
+ await writeFile(join(outputDir, 'vercel.json'), generateVercelConfig());
646
+ await writeFile(join(outputDir, 'netlify.toml'), generateNetlifyConfig());
647
+ await writeFile(join(outputDir, '.env.example'), generateEnvExample(allFetchCalls));
648
+ const ghDir = join(outputDir, '.github', 'workflows');
649
+ await mkdir(ghDir, { recursive: true });
650
+ await writeFile(join(ghDir, 'ci.yml'), generateGithubActionsCI(framework));
651
+ logger.info('Generated deployment configs (Docker, CI/CD, Vercel, Netlify, nginx)');
652
+ }
653
+ // === Generate SEO files ===
654
+ if (options.generateSeo) {
655
+ const { generateSitemap, generateRobotsTxt, generateManifest, enhanceHeadMeta } = await import('./generators/seo-generator.js');
656
+ const baseUrl = options.seoBaseUrl || 'https://example.com';
657
+ const publicDir = join(outputDir, 'public');
658
+ await mkdir(publicDir, { recursive: true });
659
+ // Build route configs for sitemap
660
+ if (needsRouter) {
661
+ const routes = generateRoutes(componentNames, fileNames);
662
+ await writeFile(join(publicDir, 'sitemap.xml'), generateSitemap(routes, baseUrl));
663
+ }
664
+ else {
665
+ await writeFile(join(publicDir, 'sitemap.xml'), generateSitemap([{ path: '/', componentName: componentNames[0] || 'App', fileName: 'index.html', isIndex: true }], baseUrl));
666
+ }
667
+ await writeFile(join(publicDir, 'robots.txt'), generateRobotsTxt(baseUrl));
668
+ await writeFile(join(publicDir, 'manifest.json'), generateManifest(mainParsed?.title || projectName, mainParsed?.meta?.['description']));
669
+ logger.info('Generated SEO files (sitemap, robots.txt, manifest)');
670
+ }
671
+ // === Accessibility enhancements ===
672
+ if (options.enhanceA11y && framework === 'react') {
673
+ try {
674
+ const { enhanceAccessibility, addSkipNavigation, generateFocusTrapHook } = await import('./generators/accessibility-generator.js');
675
+ // Enhance each component file with accessibility improvements
676
+ const compDir = join(outputDir, 'src', 'components');
677
+ const compFiles = await readdir(compDir).catch(() => []);
678
+ for (const file of compFiles) {
679
+ if (typeof file === 'string' && file.endsWith('.tsx') && !file.endsWith('.test.tsx')) {
680
+ const filePath = join(compDir, file);
681
+ let content = await readFile(filePath, 'utf-8');
682
+ content = enhanceAccessibility(content);
683
+ await writeFile(filePath, content);
684
+ }
685
+ }
686
+ // Add skip navigation component
687
+ await writeFile(join(compDir, 'skip-navigation.tsx'), addSkipNavigation());
688
+ // Add focus trap hook
689
+ const hooksDir = join(outputDir, 'src', 'hooks');
690
+ await mkdir(hooksDir, { recursive: true });
691
+ await writeFile(join(hooksDir, 'useFocusTrap.ts'), generateFocusTrapHook());
692
+ logger.info('Enhanced accessibility (ARIA labels, semantic HTML, skip nav, focus trap)');
693
+ }
694
+ catch (err) {
695
+ result.warnings.push(`Accessibility enhancement failed: ${err instanceof Error ? err.message : String(err)}`);
696
+ }
697
+ }
698
+ // === Performance optimizations ===
699
+ if (options.enhancePerf && framework === 'react') {
700
+ try {
701
+ const { addLazyLoading, generatePerformanceUtils, generateLoadingFallback, generateCodeSplitConfig } = await import('./generators/performance-generator.js');
702
+ // Add lazy loading to App.tsx if multi-page
703
+ if (needsRouter) {
704
+ const appPath = join(outputDir, 'src', 'App.tsx');
705
+ let appContent = await readFile(appPath, 'utf-8');
706
+ appContent = addLazyLoading(appContent, componentNames);
707
+ await writeFile(appPath, appContent);
708
+ }
709
+ // Generate utility files
710
+ const utilsDir = join(outputDir, 'src', 'utils');
711
+ await mkdir(utilsDir, { recursive: true });
712
+ await writeFile(join(utilsDir, 'performance.ts'), generatePerformanceUtils());
713
+ const compDir = join(outputDir, 'src', 'components');
714
+ await writeFile(join(compDir, 'loading.tsx'), generateLoadingFallback());
715
+ logger.info('Added performance optimizations (lazy loading, code splitting, utilities)');
716
+ }
717
+ catch (err) {
718
+ result.warnings.push(`Performance optimization failed: ${err instanceof Error ? err.message : String(err)}`);
719
+ }
720
+ }
721
+ // === Form system ===
722
+ if (options.generateForms && framework === 'react') {
723
+ try {
724
+ const { detectFormFields, generateFormHook, generateFormValidation } = await import('./generators/form-generator.js');
725
+ // Generate form hook
726
+ const hooksDir = join(outputDir, 'src', 'hooks');
727
+ await mkdir(hooksDir, { recursive: true });
728
+ // Detect forms across all parsed HTML
729
+ const allFormFields = parsedFiles.flatMap(p => detectFormFields(p.bodyContent));
730
+ if (allFormFields.length > 0) {
731
+ await writeFile(join(hooksDir, 'useForm.ts'), generateFormHook(allFormFields));
732
+ await writeFile(join(hooksDir, 'validation.ts'), generateFormValidation(allFormFields));
733
+ logger.info(`Generated form handling for ${allFormFields.length} field(s)`);
734
+ }
735
+ }
736
+ catch (err) {
737
+ result.warnings.push(`Form generation failed: ${err instanceof Error ? err.message : String(err)}`);
738
+ }
739
+ }
740
+ // === Type definitions ===
741
+ if (options.generateTypes && framework === 'react') {
742
+ try {
743
+ const { generateGlobalTypes, generateTypeDefinitions, inferApiResponseType } = await import('./generators/type-generator.js');
744
+ // Generate global type definitions
745
+ const typesDir = join(outputDir, 'src', 'types');
746
+ await mkdir(typesDir, { recursive: true });
747
+ await writeFile(join(typesDir, 'global.d.ts'), generateGlobalTypes());
748
+ // Generate API response types from fetch calls
749
+ const allFetchCalls = analyses.flatMap(a => a.fetchCalls);
750
+ if (allFetchCalls.length > 0) {
751
+ await writeFile(join(typesDir, 'api.ts'), inferApiResponseType(allFetchCalls));
752
+ }
753
+ // Generate component-specific types
754
+ for (let i = 0; i < parsedFiles.length; i++) {
755
+ const comp = generateComponent(parsedFiles[i], cssModules, analyses[i]);
756
+ const typeDefs = generateTypeDefinitions(comp, analyses[i]);
757
+ if (typeDefs.trim()) {
758
+ await writeFile(join(typesDir, `${toKebabCase(comp.name)}.types.ts`), typeDefs);
759
+ }
760
+ }
761
+ logger.info('Generated TypeScript type definitions');
762
+ }
763
+ catch (err) {
764
+ result.warnings.push(`Type generation failed: ${err instanceof Error ? err.message : String(err)}`);
765
+ }
766
+ }
767
+ // === Collect output stats ===
768
+ const elapsedMs = Date.now() - startTime;
769
+ const outputStats = await collectOutputStats(outputDir);
770
+ // === Generate report ===
771
+ const report = generateReport(result, analyses, tailwindDetection, {
772
+ customHooks: neededHooks,
773
+ routeCount,
774
+ formatted: formattedCount > 0,
775
+ extractedComponents: extractedComponentCount,
776
+ interactivePatterns: mergedInteractive,
777
+ elapsedMs,
778
+ outputFiles: outputStats.files,
779
+ outputLines: outputStats.lines,
780
+ });
781
+ return { ...result, report };
782
+ }
783
+ async function discoverHTMLFiles(dir) {
784
+ const files = [];
785
+ const entries = await readdir(dir, { withFileTypes: true });
786
+ for (const entry of entries) {
787
+ const fullPath = join(dir, entry.name);
788
+ if (entry.isDirectory() && !['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
789
+ const nested = await discoverHTMLFiles(fullPath);
790
+ files.push(...nested);
791
+ }
792
+ else if (entry.isFile() && ['.html', '.htm'].includes(extname(entry.name).toLowerCase())) {
793
+ files.push(fullPath);
794
+ }
795
+ }
796
+ return files;
797
+ }
798
+ /**
799
+ * Count all generated files and lines of code in the output project.
800
+ */
801
+ async function collectOutputStats(outputDir) {
802
+ let files = 0;
803
+ let lines = 0;
804
+ const CODE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.css', '.html', '.json', '.mjs', '.cjs']);
805
+ async function walk(dir) {
806
+ let entries;
807
+ try {
808
+ entries = await readdir(dir, { withFileTypes: true });
809
+ }
810
+ catch {
811
+ return;
812
+ }
813
+ for (const entry of entries) {
814
+ const fullPath = join(dir, entry.name);
815
+ if (entry.isDirectory()) {
816
+ if (entry.name === 'node_modules' || entry.name === '.git')
817
+ continue;
818
+ await walk(fullPath);
819
+ }
820
+ else if (entry.isFile()) {
821
+ const ext = extname(entry.name).toLowerCase();
822
+ if (CODE_EXTS.has(ext)) {
823
+ files++;
824
+ try {
825
+ const content = await readFile(fullPath, 'utf-8');
826
+ lines += content.split('\n').length;
827
+ }
828
+ catch { /* skip unreadable */ }
829
+ }
830
+ }
831
+ }
832
+ }
833
+ await walk(outputDir);
834
+ return { files, lines };
835
+ }
836
+ //# sourceMappingURL=converter.js.map