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,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Component Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses React (.tsx/.jsx), Vue (.vue SFC), Svelte (.svelte), Angular (.ts @Component),
|
|
5
|
+
* and Next.js components back into structured data for reverse conversion to HTML/CSS/JS.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, readdir, stat } from 'fs/promises';
|
|
8
|
+
import { join, extname, basename, dirname, relative } from 'path';
|
|
9
|
+
// ─── Framework Detection ────────────────────────────────────────────
|
|
10
|
+
export async function detectFramework(projectDir) {
|
|
11
|
+
try {
|
|
12
|
+
const pkgJson = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf-8'));
|
|
13
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
14
|
+
if (allDeps['next'])
|
|
15
|
+
return 'nextjs';
|
|
16
|
+
if (allDeps['@angular/core'])
|
|
17
|
+
return 'angular';
|
|
18
|
+
if (allDeps['svelte'] || allDeps['@sveltejs/kit'])
|
|
19
|
+
return 'svelte';
|
|
20
|
+
if (allDeps['vue'])
|
|
21
|
+
return 'vue';
|
|
22
|
+
if (allDeps['react'])
|
|
23
|
+
return 'react';
|
|
24
|
+
}
|
|
25
|
+
catch { /* no package.json */ }
|
|
26
|
+
// Fallback: check for framework-specific config files
|
|
27
|
+
try {
|
|
28
|
+
await stat(join(projectDir, 'next.config.ts'));
|
|
29
|
+
return 'nextjs';
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
try {
|
|
33
|
+
await stat(join(projectDir, 'next.config.js'));
|
|
34
|
+
return 'nextjs';
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
try {
|
|
38
|
+
await stat(join(projectDir, 'angular.json'));
|
|
39
|
+
return 'angular';
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
try {
|
|
43
|
+
await stat(join(projectDir, 'svelte.config.js'));
|
|
44
|
+
return 'svelte';
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
try {
|
|
48
|
+
await stat(join(projectDir, 'vite.config.ts'));
|
|
49
|
+
// Could be React or Vue — check for vue plugin
|
|
50
|
+
const config = await readFile(join(projectDir, 'vite.config.ts'), 'utf-8');
|
|
51
|
+
if (config.includes('@vitejs/plugin-vue'))
|
|
52
|
+
return 'vue';
|
|
53
|
+
return 'react';
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
return 'react'; // default
|
|
57
|
+
}
|
|
58
|
+
// ─── Main Project Parser ────────────────────────────────────────────
|
|
59
|
+
export async function parseFrameworkProject(projectDir) {
|
|
60
|
+
const framework = await detectFramework(projectDir);
|
|
61
|
+
const result = {
|
|
62
|
+
framework,
|
|
63
|
+
projectName: basename(projectDir),
|
|
64
|
+
components: [],
|
|
65
|
+
routes: [],
|
|
66
|
+
globalCSS: [],
|
|
67
|
+
externalStyles: [],
|
|
68
|
+
externalScripts: [],
|
|
69
|
+
preconnectLinks: [],
|
|
70
|
+
title: '',
|
|
71
|
+
meta: {},
|
|
72
|
+
};
|
|
73
|
+
// Parse project title and meta from index.html or equivalent
|
|
74
|
+
await parseProjectMeta(projectDir, framework, result);
|
|
75
|
+
// Discover and parse components
|
|
76
|
+
const componentFiles = await discoverComponentFiles(projectDir, framework);
|
|
77
|
+
for (const file of componentFiles) {
|
|
78
|
+
try {
|
|
79
|
+
const content = await readFile(file, 'utf-8');
|
|
80
|
+
const relPath = relative(projectDir, file);
|
|
81
|
+
const component = parseComponentFile(content, relPath, framework);
|
|
82
|
+
if (component) {
|
|
83
|
+
result.components.push(component);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch { /* skip unparseable files */ }
|
|
87
|
+
}
|
|
88
|
+
// Parse routes
|
|
89
|
+
result.routes = await parseRoutes(projectDir, framework, result.components);
|
|
90
|
+
// Collect global CSS
|
|
91
|
+
await collectGlobalCSS(projectDir, framework, result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
// ─── Component File Discovery ───────────────────────────────────────
|
|
95
|
+
async function discoverComponentFiles(projectDir, framework) {
|
|
96
|
+
const files = [];
|
|
97
|
+
const extensions = getComponentExtensions(framework);
|
|
98
|
+
let searchDirs;
|
|
99
|
+
switch (framework) {
|
|
100
|
+
case 'nextjs':
|
|
101
|
+
searchDirs = [join(projectDir, 'app'), join(projectDir, 'src')];
|
|
102
|
+
break;
|
|
103
|
+
case 'svelte':
|
|
104
|
+
searchDirs = [join(projectDir, 'src')];
|
|
105
|
+
break;
|
|
106
|
+
case 'angular':
|
|
107
|
+
searchDirs = [join(projectDir, 'src', 'app')];
|
|
108
|
+
break;
|
|
109
|
+
case 'vue':
|
|
110
|
+
searchDirs = [join(projectDir, 'src')];
|
|
111
|
+
break;
|
|
112
|
+
default: // react
|
|
113
|
+
searchDirs = [join(projectDir, 'src')];
|
|
114
|
+
}
|
|
115
|
+
for (const dir of searchDirs) {
|
|
116
|
+
await walkDir(dir, extensions, files);
|
|
117
|
+
}
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
function getComponentExtensions(framework) {
|
|
121
|
+
switch (framework) {
|
|
122
|
+
case 'vue': return ['.vue'];
|
|
123
|
+
case 'svelte': return ['.svelte'];
|
|
124
|
+
case 'angular': return ['.ts']; // Angular components are .ts
|
|
125
|
+
default: return ['.tsx', '.jsx'];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function walkDir(dir, extensions, result) {
|
|
129
|
+
let entries;
|
|
130
|
+
try {
|
|
131
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const fullPath = join(dir, entry.name);
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
if (['node_modules', '.git', 'dist', 'build', '.next', '.svelte-kit', '.angular'].includes(entry.name))
|
|
140
|
+
continue;
|
|
141
|
+
await walkDir(fullPath, extensions, result);
|
|
142
|
+
}
|
|
143
|
+
else if (entry.isFile()) {
|
|
144
|
+
const ext = extname(entry.name);
|
|
145
|
+
if (extensions.includes(ext)) {
|
|
146
|
+
// Skip config/utility files
|
|
147
|
+
if (entry.name.startsWith('vite.config') || entry.name.startsWith('next.config') ||
|
|
148
|
+
entry.name === 'main.ts' || entry.name === 'main.tsx' ||
|
|
149
|
+
entry.name.endsWith('.d.ts') || entry.name.endsWith('.spec.ts') ||
|
|
150
|
+
entry.name.endsWith('.test.ts') || entry.name.endsWith('.test.tsx'))
|
|
151
|
+
continue;
|
|
152
|
+
result.push(fullPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ─── Component Parsing ──────────────────────────────────────────────
|
|
158
|
+
function parseComponentFile(content, relPath, framework) {
|
|
159
|
+
switch (framework) {
|
|
160
|
+
case 'react':
|
|
161
|
+
case 'nextjs':
|
|
162
|
+
return parseReactComponent(content, relPath, framework);
|
|
163
|
+
case 'vue':
|
|
164
|
+
return parseVueComponent(content, relPath);
|
|
165
|
+
case 'svelte':
|
|
166
|
+
return parseSvelteComponent(content, relPath);
|
|
167
|
+
case 'angular':
|
|
168
|
+
return parseAngularComponent(content, relPath);
|
|
169
|
+
default:
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ─── React/Next.js Parser ───────────────────────────────────────────
|
|
174
|
+
function parseReactComponent(content, relPath, framework) {
|
|
175
|
+
const name = extractComponentName(content, relPath);
|
|
176
|
+
if (!name)
|
|
177
|
+
return null;
|
|
178
|
+
const stateVars = new Map();
|
|
179
|
+
const eventHandlers = new Map();
|
|
180
|
+
const effects = [];
|
|
181
|
+
const childComponents = [];
|
|
182
|
+
// Extract useState declarations
|
|
183
|
+
const statePattern = /const\s+\[(\w+),\s*(\w+)\]\s*=\s*useState(?:<[^>]*>)?\(([^)]*)\)/g;
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = statePattern.exec(content)) !== null) {
|
|
186
|
+
stateVars.set(match[1], match[3] || "''");
|
|
187
|
+
}
|
|
188
|
+
// Extract useEffect
|
|
189
|
+
const effectPattern = /useEffect\(\(\)\s*=>\s*\{([\s\S]*?)\},\s*\[[^\]]*\]\)/g;
|
|
190
|
+
while ((match = effectPattern.exec(content)) !== null) {
|
|
191
|
+
effects.push(match[1].trim());
|
|
192
|
+
}
|
|
193
|
+
// Extract event handlers: const handleX/toggleX/onX = (...) => { ... }
|
|
194
|
+
const handlerPattern = /const\s+(handle\w+|toggle\w+|on[A-Z]\w+)\s*=\s*(?:\([^)]*\)\s*=>|\(\)\s*=>)\s*\{([\s\S]*?)\n\s*\};/g;
|
|
195
|
+
while ((match = handlerPattern.exec(content)) !== null) {
|
|
196
|
+
eventHandlers.set(match[1], match[2].trim());
|
|
197
|
+
}
|
|
198
|
+
// Also extract function declaration handlers
|
|
199
|
+
const fnHandlerPattern = /function\s+(handle\w+|toggle\w+|on[A-Z]\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/g;
|
|
200
|
+
while ((match = fnHandlerPattern.exec(content)) !== null) {
|
|
201
|
+
if (!eventHandlers.has(match[1])) {
|
|
202
|
+
eventHandlers.set(match[1], match[2].trim());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Extract JSX from return statement
|
|
206
|
+
const template = extractJSXReturn(content);
|
|
207
|
+
// Extract CSS imports
|
|
208
|
+
let css = '';
|
|
209
|
+
const cssImportPattern = /import\s+['"]\.\/([^'"]+\.css)['"]/g;
|
|
210
|
+
while ((match = cssImportPattern.exec(content)) !== null) {
|
|
211
|
+
// CSS will be collected separately
|
|
212
|
+
}
|
|
213
|
+
// Detect child component imports
|
|
214
|
+
const importPattern = /import\s+(\w+)\s+from\s+['"]\.\/([^'"]+)['"]/g;
|
|
215
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
216
|
+
if (/^[A-Z]/.test(match[1])) {
|
|
217
|
+
childComponents.push(match[1]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Detect Link imports (react-router or next/link)
|
|
221
|
+
const usesLink = /import.*Link.*from\s+['"](?:react-router-dom|next\/link)['"]/.test(content);
|
|
222
|
+
return {
|
|
223
|
+
filePath: relPath,
|
|
224
|
+
name,
|
|
225
|
+
framework,
|
|
226
|
+
template: template || '',
|
|
227
|
+
css,
|
|
228
|
+
script: '',
|
|
229
|
+
stateVars,
|
|
230
|
+
eventHandlers,
|
|
231
|
+
effects,
|
|
232
|
+
isInteractive: stateVars.size > 0 || eventHandlers.size > 0 || effects.length > 0,
|
|
233
|
+
childComponents,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function extractComponentName(content, relPath) {
|
|
237
|
+
// export default function ComponentName
|
|
238
|
+
let match = content.match(/(?:export\s+default\s+)?function\s+([A-Z]\w+)\s*\(/);
|
|
239
|
+
if (match)
|
|
240
|
+
return match[1];
|
|
241
|
+
// const ComponentName = ...
|
|
242
|
+
match = content.match(/(?:export\s+default\s+)?const\s+([A-Z]\w+)\s*[=:]/);
|
|
243
|
+
if (match)
|
|
244
|
+
return match[1];
|
|
245
|
+
// export default ComponentName
|
|
246
|
+
match = content.match(/export\s+default\s+([A-Z]\w+)/);
|
|
247
|
+
if (match)
|
|
248
|
+
return match[1];
|
|
249
|
+
// Derive from filename
|
|
250
|
+
const base = basename(relPath).replace(/\.\w+$/, '');
|
|
251
|
+
if (/^[A-Z]/.test(base))
|
|
252
|
+
return base;
|
|
253
|
+
return base.charAt(0).toUpperCase() + base.slice(1).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
254
|
+
}
|
|
255
|
+
function extractJSXReturn(content) {
|
|
256
|
+
// Find the LAST `return (` that contains JSX (the component's main return)
|
|
257
|
+
// Search from the end of the file backwards to skip handler returns
|
|
258
|
+
const returnPattern = /return\s*\(/g;
|
|
259
|
+
const candidates = [];
|
|
260
|
+
let match;
|
|
261
|
+
while ((match = returnPattern.exec(content)) !== null) {
|
|
262
|
+
candidates.push(match.index);
|
|
263
|
+
}
|
|
264
|
+
// Try each candidate from LAST to FIRST — the component's main return
|
|
265
|
+
// is usually the last one (closest to the end of the file)
|
|
266
|
+
for (let ci = candidates.length - 1; ci >= 0; ci--) {
|
|
267
|
+
const returnIdx = candidates[ci];
|
|
268
|
+
const start = content.indexOf('(', returnIdx);
|
|
269
|
+
if (start === -1)
|
|
270
|
+
continue;
|
|
271
|
+
let depth = 1;
|
|
272
|
+
let i = start + 1;
|
|
273
|
+
// Track JSX brace depth to know when we're in a JS expression vs text
|
|
274
|
+
let braceDepth = 0;
|
|
275
|
+
// Track whether we're inside an HTML tag definition (between < and >)
|
|
276
|
+
let inTag = false;
|
|
277
|
+
while (i < content.length && depth > 0) {
|
|
278
|
+
// Skip /* ... */ comments
|
|
279
|
+
if (content[i] === '/' && content[i + 1] === '*') {
|
|
280
|
+
i += 2;
|
|
281
|
+
while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) {
|
|
282
|
+
i++;
|
|
283
|
+
}
|
|
284
|
+
i += 2;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Skip // line comments
|
|
288
|
+
if (content[i] === '/' && content[i + 1] === '/') {
|
|
289
|
+
while (i < content.length && content[i] !== '\n')
|
|
290
|
+
i++;
|
|
291
|
+
i++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
// Track JSX braces for expression context
|
|
295
|
+
if (content[i] === '{') {
|
|
296
|
+
braceDepth++;
|
|
297
|
+
i++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (content[i] === '}' && braceDepth > 0) {
|
|
301
|
+
braceDepth--;
|
|
302
|
+
i++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
// Track HTML tag context
|
|
306
|
+
if (content[i] === '<' && braceDepth === 0) {
|
|
307
|
+
inTag = true;
|
|
308
|
+
}
|
|
309
|
+
if (content[i] === '>' && inTag && braceDepth === 0) {
|
|
310
|
+
inTag = false;
|
|
311
|
+
}
|
|
312
|
+
// Handle string literals — only when inside a JS expression ({...}) or tag attribute
|
|
313
|
+
// In JSX text content (between > and <), quotes are just text, not string delimiters
|
|
314
|
+
if ((content[i] === '"' || content[i] === "'" || content[i] === '`') && (braceDepth > 0 || inTag)) {
|
|
315
|
+
const quote = content[i];
|
|
316
|
+
i++;
|
|
317
|
+
while (i < content.length && content[i] !== quote) {
|
|
318
|
+
if (content[i] === '\\')
|
|
319
|
+
i++; // skip escaped chars
|
|
320
|
+
i++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (content[i] === '(') {
|
|
324
|
+
depth++;
|
|
325
|
+
}
|
|
326
|
+
else if (content[i] === ')') {
|
|
327
|
+
depth--;
|
|
328
|
+
}
|
|
329
|
+
i++;
|
|
330
|
+
}
|
|
331
|
+
if (depth !== 0)
|
|
332
|
+
continue;
|
|
333
|
+
let jsx = content.slice(start + 1, i - 1).trim();
|
|
334
|
+
// Check if this actually contains JSX (has HTML-like tags)
|
|
335
|
+
if (/<\w/.test(jsx) || /^\s*</.test(jsx)) {
|
|
336
|
+
// Remove wrapping fragments
|
|
337
|
+
jsx = jsx
|
|
338
|
+
.replace(/^\s*<>\s*/, '')
|
|
339
|
+
.replace(/\s*<\/>\s*$/, '')
|
|
340
|
+
.replace(/^\s*<React\.Fragment>\s*/, '')
|
|
341
|
+
.replace(/\s*<\/React\.Fragment>\s*$/, '')
|
|
342
|
+
.trim();
|
|
343
|
+
// Remove any trailing non-HTML code leaked after the template
|
|
344
|
+
// (catches export statements, multi-line comments, etc.)
|
|
345
|
+
// Split by lines and find where non-HTML content starts
|
|
346
|
+
const jsxLines = jsx.split('\n');
|
|
347
|
+
let truncateAt = jsxLines.length;
|
|
348
|
+
for (let li = jsxLines.length - 1; li >= 0; li--) {
|
|
349
|
+
const line = jsxLines[li].trim();
|
|
350
|
+
if (!line)
|
|
351
|
+
continue; // skip blank lines
|
|
352
|
+
// If line looks like JS code (export, function, var, const, /*, etc.) not HTML
|
|
353
|
+
if (/^(export\s|function\s|const\s|let\s|var\s|\/\*|\/\/|\*\s*$|\*\s+|^\*\/|import\s|}\s*;?\s*$)/.test(line) || line.startsWith('*')) {
|
|
354
|
+
truncateAt = li;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
break; // Found a line that looks like HTML, stop
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (truncateAt < jsxLines.length) {
|
|
361
|
+
jsx = jsxLines.slice(0, truncateAt).join('\n').trim();
|
|
362
|
+
}
|
|
363
|
+
return jsx;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
// ─── Vue SFC Parser ─────────────────────────────────────────────────
|
|
369
|
+
function parseVueComponent(content, relPath) {
|
|
370
|
+
const name = basename(relPath).replace(/\.vue$/, '');
|
|
371
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
372
|
+
const stateVars = new Map();
|
|
373
|
+
const eventHandlers = new Map();
|
|
374
|
+
const effects = [];
|
|
375
|
+
const childComponents = [];
|
|
376
|
+
// Extract <template> content
|
|
377
|
+
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
|
|
378
|
+
const template = templateMatch ? templateMatch[1].trim() : '';
|
|
379
|
+
// Extract <script setup> content
|
|
380
|
+
const scriptMatch = content.match(/<script\s+setup[^>]*>([\s\S]*?)<\/script>/);
|
|
381
|
+
const script = scriptMatch ? scriptMatch[1].trim() : '';
|
|
382
|
+
// Extract <style> content
|
|
383
|
+
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/);
|
|
384
|
+
const css = styleMatch ? styleMatch[1].trim() : '';
|
|
385
|
+
// Parse ref() declarations from script
|
|
386
|
+
const refPattern = /const\s+(\w+)\s*=\s*ref(?:<[^>]*>)?\(([^)]*)\)/g;
|
|
387
|
+
let match;
|
|
388
|
+
while ((match = refPattern.exec(script)) !== null) {
|
|
389
|
+
stateVars.set(match[1], match[2] || "''");
|
|
390
|
+
}
|
|
391
|
+
// Parse event handlers
|
|
392
|
+
const handlerPattern = /const\s+(\w+)\s*=\s*(?:\([^)]*\)\s*=>|\(\)\s*=>)\s*\{([\s\S]*?)\n\s*\};?/g;
|
|
393
|
+
while ((match = handlerPattern.exec(script)) !== null) {
|
|
394
|
+
eventHandlers.set(match[1], match[2].trim());
|
|
395
|
+
}
|
|
396
|
+
// Parse function handlers
|
|
397
|
+
const fnHandlerPattern = /function\s+(\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/g;
|
|
398
|
+
while ((match = fnHandlerPattern.exec(script)) !== null) {
|
|
399
|
+
eventHandlers.set(match[1], match[2].trim());
|
|
400
|
+
}
|
|
401
|
+
// Parse onMounted effects
|
|
402
|
+
const mountedPattern = /onMounted\(\(\)\s*=>\s*\{([\s\S]*?)\}\)/g;
|
|
403
|
+
while ((match = mountedPattern.exec(script)) !== null) {
|
|
404
|
+
effects.push(match[1].trim());
|
|
405
|
+
}
|
|
406
|
+
// Import detection
|
|
407
|
+
const importPattern = /import\s+(\w+)\s+from\s+['"][^'"]*['"]/g;
|
|
408
|
+
while ((match = importPattern.exec(script)) !== null) {
|
|
409
|
+
if (/^[A-Z]/.test(match[1]))
|
|
410
|
+
childComponents.push(match[1]);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
filePath: relPath,
|
|
414
|
+
name: pascalName,
|
|
415
|
+
framework: 'vue',
|
|
416
|
+
template,
|
|
417
|
+
css,
|
|
418
|
+
script,
|
|
419
|
+
stateVars,
|
|
420
|
+
eventHandlers,
|
|
421
|
+
effects,
|
|
422
|
+
isInteractive: stateVars.size > 0 || eventHandlers.size > 0 || effects.length > 0,
|
|
423
|
+
childComponents,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// ─── Svelte Parser ──────────────────────────────────────────────────
|
|
427
|
+
function parseSvelteComponent(content, relPath) {
|
|
428
|
+
const name = basename(relPath).replace(/\.svelte$/, '');
|
|
429
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
430
|
+
const stateVars = new Map();
|
|
431
|
+
const eventHandlers = new Map();
|
|
432
|
+
const effects = [];
|
|
433
|
+
const childComponents = [];
|
|
434
|
+
// Extract <script> content
|
|
435
|
+
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
436
|
+
const script = scriptMatch ? scriptMatch[1].trim() : '';
|
|
437
|
+
// Extract <style> content
|
|
438
|
+
const styleMatch = content.match(/<style[^>]*>([\s\S]*?)<\/style>/);
|
|
439
|
+
const css = styleMatch ? styleMatch[1].trim() : '';
|
|
440
|
+
// Template is everything outside script and style tags
|
|
441
|
+
let template = content
|
|
442
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/g, '')
|
|
443
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/g, '')
|
|
444
|
+
.trim();
|
|
445
|
+
// Parse $state() declarations
|
|
446
|
+
const statePattern = /let\s+(\w+)\s*=\s*\$state\(([^)]*)\)/g;
|
|
447
|
+
let match;
|
|
448
|
+
while ((match = statePattern.exec(script)) !== null) {
|
|
449
|
+
stateVars.set(match[1], match[2] || "''");
|
|
450
|
+
}
|
|
451
|
+
// Parse regular let declarations (Svelte 4 reactive)
|
|
452
|
+
const letPattern = /let\s+(\w+)\s*=\s*([^;]+);/g;
|
|
453
|
+
while ((match = letPattern.exec(script)) !== null) {
|
|
454
|
+
if (!match[2].includes('$state') && !match[2].includes('import')) {
|
|
455
|
+
stateVars.set(match[1], match[2].trim());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Parse function handlers
|
|
459
|
+
const fnPattern = /function\s+(\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/g;
|
|
460
|
+
while ((match = fnPattern.exec(script)) !== null) {
|
|
461
|
+
eventHandlers.set(match[1], match[2].trim());
|
|
462
|
+
}
|
|
463
|
+
// Parse $effect()
|
|
464
|
+
const effectPattern = /\$effect\(\(\)\s*=>\s*\{([\s\S]*?)\}\)/g;
|
|
465
|
+
while ((match = effectPattern.exec(script)) !== null) {
|
|
466
|
+
effects.push(match[1].trim());
|
|
467
|
+
}
|
|
468
|
+
// Parse onMount
|
|
469
|
+
const mountPattern = /onMount\(\(\)\s*=>\s*\{([\s\S]*?)\}\)/g;
|
|
470
|
+
while ((match = mountPattern.exec(script)) !== null) {
|
|
471
|
+
effects.push(match[1].trim());
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
filePath: relPath,
|
|
475
|
+
name: pascalName,
|
|
476
|
+
framework: 'svelte',
|
|
477
|
+
template,
|
|
478
|
+
css,
|
|
479
|
+
script,
|
|
480
|
+
stateVars,
|
|
481
|
+
eventHandlers,
|
|
482
|
+
effects,
|
|
483
|
+
isInteractive: stateVars.size > 0 || eventHandlers.size > 0 || effects.length > 0,
|
|
484
|
+
childComponents,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// ─── Angular Parser ─────────────────────────────────────────────────
|
|
488
|
+
function parseAngularComponent(content, relPath) {
|
|
489
|
+
// Must have @Component decorator
|
|
490
|
+
if (!content.includes('@Component'))
|
|
491
|
+
return null;
|
|
492
|
+
const name = basename(relPath).replace(/\.component\.ts$/, '').replace(/\.ts$/, '');
|
|
493
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
494
|
+
const stateVars = new Map();
|
|
495
|
+
const eventHandlers = new Map();
|
|
496
|
+
const effects = [];
|
|
497
|
+
const childComponents = [];
|
|
498
|
+
// Extract inline template
|
|
499
|
+
const templateMatch = content.match(/template:\s*`([\s\S]*?)`/);
|
|
500
|
+
const template = templateMatch ? templateMatch[1].trim() : '';
|
|
501
|
+
// Extract inline styles
|
|
502
|
+
const stylesMatch = content.match(/styles:\s*\[\s*`([\s\S]*?)`\s*\]/);
|
|
503
|
+
const css = stylesMatch ? stylesMatch[1].trim() : '';
|
|
504
|
+
// Parse signal() declarations
|
|
505
|
+
const signalPattern = /(\w+)\s*=\s*signal(?:<[^>]*>)?\(([^)]*)\)/g;
|
|
506
|
+
let match;
|
|
507
|
+
while ((match = signalPattern.exec(content)) !== null) {
|
|
508
|
+
stateVars.set(match[1], match[2] || "''");
|
|
509
|
+
}
|
|
510
|
+
// Parse methods (event handlers)
|
|
511
|
+
const methodPattern = /(\w+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/g;
|
|
512
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
513
|
+
const methodName = match[1];
|
|
514
|
+
if (['constructor', 'ngOnInit', 'ngOnDestroy', 'ngAfterViewInit'].includes(methodName)) {
|
|
515
|
+
if (methodName === 'ngOnInit')
|
|
516
|
+
effects.push(match[2].trim());
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
eventHandlers.set(methodName, match[2].trim());
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
filePath: relPath,
|
|
523
|
+
name: pascalName + 'Component',
|
|
524
|
+
framework: 'angular',
|
|
525
|
+
template,
|
|
526
|
+
css,
|
|
527
|
+
script: '',
|
|
528
|
+
stateVars,
|
|
529
|
+
eventHandlers,
|
|
530
|
+
effects,
|
|
531
|
+
isInteractive: stateVars.size > 0 || eventHandlers.size > 0 || effects.length > 0,
|
|
532
|
+
childComponents,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
// ─── Route Parsing ──────────────────────────────────────────────────
|
|
536
|
+
async function parseRoutes(projectDir, framework, components) {
|
|
537
|
+
const routes = [];
|
|
538
|
+
switch (framework) {
|
|
539
|
+
case 'react': {
|
|
540
|
+
// Parse App.tsx for React Router routes
|
|
541
|
+
try {
|
|
542
|
+
const appContent = await readFile(join(projectDir, 'src', 'App.tsx'), 'utf-8');
|
|
543
|
+
const routePattern = /<Route\s+path="([^"]+)"\s+element=\{<(\w+)\s*\/>\}/g;
|
|
544
|
+
let match;
|
|
545
|
+
while ((match = routePattern.exec(appContent)) !== null) {
|
|
546
|
+
routes.push({
|
|
547
|
+
path: match[1],
|
|
548
|
+
componentName: match[2],
|
|
549
|
+
componentFile: '',
|
|
550
|
+
isIndex: match[1] === '/',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch { /* no App.tsx or no routes */ }
|
|
555
|
+
// If no routes found, treat it as single-page
|
|
556
|
+
if (routes.length === 0 && components.length > 0) {
|
|
557
|
+
routes.push({
|
|
558
|
+
path: '/',
|
|
559
|
+
componentName: components[0].name,
|
|
560
|
+
componentFile: components[0].filePath,
|
|
561
|
+
isIndex: true,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'nextjs': {
|
|
567
|
+
// File-based routing: app/page.tsx, app/about/page.tsx
|
|
568
|
+
const appDir = join(projectDir, 'app');
|
|
569
|
+
const pageFiles = await findFiles(appDir, 'page.tsx');
|
|
570
|
+
for (const file of pageFiles) {
|
|
571
|
+
const relDir = relative(appDir, dirname(file));
|
|
572
|
+
const path = relDir === '' || relDir === '.' ? '/' : `/${relDir}`;
|
|
573
|
+
const content = await readFile(file, 'utf-8');
|
|
574
|
+
const nameMatch = content.match(/import\s+(\w+)\s+from/);
|
|
575
|
+
routes.push({
|
|
576
|
+
path,
|
|
577
|
+
componentName: nameMatch?.[1] || 'Page',
|
|
578
|
+
componentFile: relative(projectDir, file),
|
|
579
|
+
isIndex: path === '/',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case 'vue': {
|
|
585
|
+
// Parse router/index.ts
|
|
586
|
+
try {
|
|
587
|
+
const routerContent = await readFile(join(projectDir, 'src', 'router', 'index.ts'), 'utf-8');
|
|
588
|
+
const routePattern = /path:\s*'([^']+)',\s*name:\s*'([^']+)'/g;
|
|
589
|
+
let match;
|
|
590
|
+
while ((match = routePattern.exec(routerContent)) !== null) {
|
|
591
|
+
routes.push({
|
|
592
|
+
path: match[1],
|
|
593
|
+
componentName: match[2],
|
|
594
|
+
componentFile: '',
|
|
595
|
+
isIndex: match[1] === '/',
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch { /* no router config */ }
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case 'svelte': {
|
|
603
|
+
// File-based routing: src/routes/+page.svelte, src/routes/about/+page.svelte
|
|
604
|
+
const routesDir = join(projectDir, 'src', 'routes');
|
|
605
|
+
const pageFiles = await findFiles(routesDir, '+page.svelte');
|
|
606
|
+
for (const file of pageFiles) {
|
|
607
|
+
const relDir = relative(routesDir, dirname(file));
|
|
608
|
+
const path = relDir === '' || relDir === '.' ? '/' : `/${relDir}`;
|
|
609
|
+
const content = await readFile(file, 'utf-8');
|
|
610
|
+
const nameMatch = content.match(/import\s+(\w+)\s+from/);
|
|
611
|
+
routes.push({
|
|
612
|
+
path,
|
|
613
|
+
componentName: nameMatch?.[1] || basename(dirname(file)),
|
|
614
|
+
componentFile: relative(projectDir, file),
|
|
615
|
+
isIndex: path === '/',
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case 'angular': {
|
|
621
|
+
// Parse app.routes.ts
|
|
622
|
+
try {
|
|
623
|
+
const routesContent = await readFile(join(projectDir, 'src', 'app', 'app.routes.ts'), 'utf-8');
|
|
624
|
+
// Match route blocks: { path: '...', loadComponent: ... .then((m) => m.ComponentName) }
|
|
625
|
+
const routePattern = /path:\s*'([^']*)'[\s\S]*?\.then\(\s*\(?m\)?\s*=>\s*m\.(\w+)/g;
|
|
626
|
+
let match;
|
|
627
|
+
while ((match = routePattern.exec(routesContent)) !== null) {
|
|
628
|
+
routes.push({
|
|
629
|
+
path: match[1] === '' ? '/' : `/${match[1]}`,
|
|
630
|
+
componentName: match[2],
|
|
631
|
+
componentFile: '',
|
|
632
|
+
isIndex: match[1] === '',
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch { /* no routes config */ }
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return routes;
|
|
641
|
+
}
|
|
642
|
+
async function findFiles(dir, fileName) {
|
|
643
|
+
const results = [];
|
|
644
|
+
let entries;
|
|
645
|
+
try {
|
|
646
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return results;
|
|
650
|
+
}
|
|
651
|
+
for (const entry of entries) {
|
|
652
|
+
const fullPath = join(dir, entry.name);
|
|
653
|
+
if (entry.isDirectory()) {
|
|
654
|
+
if (entry.name === 'node_modules')
|
|
655
|
+
continue;
|
|
656
|
+
results.push(...await findFiles(fullPath, fileName));
|
|
657
|
+
}
|
|
658
|
+
else if (entry.name === fileName) {
|
|
659
|
+
results.push(fullPath);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return results;
|
|
663
|
+
}
|
|
664
|
+
// ─── Project Meta Parsing ───────────────────────────────────────────
|
|
665
|
+
async function parseProjectMeta(projectDir, framework, result) {
|
|
666
|
+
// Try reading index.html
|
|
667
|
+
const indexPaths = [
|
|
668
|
+
join(projectDir, 'index.html'),
|
|
669
|
+
join(projectDir, 'public', 'index.html'),
|
|
670
|
+
join(projectDir, 'src', 'index.html'),
|
|
671
|
+
join(projectDir, 'src', 'app.html'), // SvelteKit
|
|
672
|
+
];
|
|
673
|
+
for (const indexPath of indexPaths) {
|
|
674
|
+
try {
|
|
675
|
+
const html = await readFile(indexPath, 'utf-8');
|
|
676
|
+
const titleMatch = html.match(/<title>([^<]*)<\/title>/);
|
|
677
|
+
if (titleMatch)
|
|
678
|
+
result.title = titleMatch[1];
|
|
679
|
+
// Extract external stylesheets (both href="..." and rel="stylesheet")
|
|
680
|
+
const linkPattern = /<link[^>]*href="(https?:\/\/[^"]+)"[^>]*>/g;
|
|
681
|
+
let match;
|
|
682
|
+
while ((match = linkPattern.exec(html)) !== null) {
|
|
683
|
+
// Skip preconnect links (they're not stylesheets)
|
|
684
|
+
if (match[0].includes('rel="preconnect"'))
|
|
685
|
+
continue;
|
|
686
|
+
// Only include if it's a stylesheet (has rel="stylesheet" or is a font link)
|
|
687
|
+
if (match[0].includes('stylesheet') || match[0].includes('fonts.googleapis')) {
|
|
688
|
+
result.externalStyles.push(match[1]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Also match: <link rel="stylesheet" href="...">
|
|
692
|
+
const linkPattern2 = /<link[^>]*rel="stylesheet"[^>]*href="(https?:\/\/[^"]+)"/g;
|
|
693
|
+
while ((match = linkPattern2.exec(html)) !== null) {
|
|
694
|
+
if (!result.externalStyles.includes(match[1])) {
|
|
695
|
+
result.externalStyles.push(match[1]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Match preconnect links (for fonts)
|
|
699
|
+
const preconnectPattern = /<link[^>]*rel="preconnect"[^>]*>/g;
|
|
700
|
+
while ((match = preconnectPattern.exec(html)) !== null) {
|
|
701
|
+
if (!result.preconnectLinks.includes(match[0])) {
|
|
702
|
+
result.preconnectLinks.push(match[0]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Extract external scripts
|
|
706
|
+
const scriptPattern = /<script[^>]*src="(https?:\/\/[^"]+)"[^>]*>/g;
|
|
707
|
+
while ((match = scriptPattern.exec(html)) !== null) {
|
|
708
|
+
result.externalScripts.push(match[1]);
|
|
709
|
+
}
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Get project name from package.json
|
|
717
|
+
try {
|
|
718
|
+
const pkg = JSON.parse(await readFile(join(projectDir, 'package.json'), 'utf-8'));
|
|
719
|
+
result.projectName = pkg.name || result.projectName;
|
|
720
|
+
}
|
|
721
|
+
catch { }
|
|
722
|
+
}
|
|
723
|
+
// ─── Global CSS Collection ──────────────────────────────────────────
|
|
724
|
+
async function collectGlobalCSS(projectDir, framework, result) {
|
|
725
|
+
const cssPaths = [];
|
|
726
|
+
switch (framework) {
|
|
727
|
+
case 'react':
|
|
728
|
+
cssPaths.push(join(projectDir, 'src', 'styles', 'global.css'), join(projectDir, 'src', 'styles', 'tailwind.css'), join(projectDir, 'src', 'index.css'), join(projectDir, 'src', 'App.css'));
|
|
729
|
+
break;
|
|
730
|
+
case 'nextjs':
|
|
731
|
+
cssPaths.push(join(projectDir, 'app', 'styles', 'global.css'), join(projectDir, 'app', 'styles', 'tailwind.css'), join(projectDir, 'app', 'globals.css'));
|
|
732
|
+
break;
|
|
733
|
+
case 'vue':
|
|
734
|
+
cssPaths.push(join(projectDir, 'src', 'styles', 'global.css'), join(projectDir, 'src', 'styles', 'tailwind.css'), join(projectDir, 'src', 'assets', 'main.css'));
|
|
735
|
+
break;
|
|
736
|
+
case 'svelte':
|
|
737
|
+
cssPaths.push(join(projectDir, 'src', 'lib', 'styles', 'global.css'), join(projectDir, 'src', 'lib', 'styles', 'tailwind.css'), join(projectDir, 'src', 'app.css'));
|
|
738
|
+
break;
|
|
739
|
+
case 'angular':
|
|
740
|
+
cssPaths.push(join(projectDir, 'src', 'styles', 'global.css'), join(projectDir, 'src', 'styles.css'));
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
for (const cssPath of cssPaths) {
|
|
744
|
+
try {
|
|
745
|
+
const css = await readFile(cssPath, 'utf-8');
|
|
746
|
+
if (css.trim())
|
|
747
|
+
result.globalCSS.push(css);
|
|
748
|
+
}
|
|
749
|
+
catch { /* file doesn't exist */ }
|
|
750
|
+
}
|
|
751
|
+
// Also collect component-level CSS files
|
|
752
|
+
const componentDir = framework === 'svelte'
|
|
753
|
+
? join(projectDir, 'src', 'lib', 'components')
|
|
754
|
+
: framework === 'angular'
|
|
755
|
+
? join(projectDir, 'src', 'app', 'components')
|
|
756
|
+
: framework === 'nextjs'
|
|
757
|
+
? join(projectDir, 'app', 'components')
|
|
758
|
+
: join(projectDir, 'src', 'components');
|
|
759
|
+
try {
|
|
760
|
+
const cssFiles = [];
|
|
761
|
+
await walkDir(componentDir, ['.css'], cssFiles);
|
|
762
|
+
for (const file of cssFiles) {
|
|
763
|
+
const css = await readFile(file, 'utf-8');
|
|
764
|
+
if (css.trim())
|
|
765
|
+
result.globalCSS.push(css);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch { }
|
|
769
|
+
}
|
|
770
|
+
//# sourceMappingURL=framework-parser.js.map
|