real-prototypes-skill 0.1.0 → 0.1.2
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/.claude/skills/real-prototypes-skill/SKILL.md +212 -16
- package/.claude/skills/real-prototypes-skill/cli.js +523 -17
- package/.claude/skills/real-prototypes-skill/scripts/detect-prototype.js +652 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-components.js +731 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-css.js +557 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-plan.js +744 -0
- package/.claude/skills/real-prototypes-skill/scripts/html-to-react.js +645 -0
- package/.claude/skills/real-prototypes-skill/scripts/inject-component.js +604 -0
- package/.claude/skills/real-prototypes-skill/scripts/project-structure.js +457 -0
- package/.claude/skills/real-prototypes-skill/scripts/visual-diff.js +474 -0
- package/.claude/skills/real-prototypes-skill/validation/color-validator.js +496 -0
- package/LICENSE +201 -21
- package/README.md +444 -444
- package/bin/cli.js +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTML-to-React Conversion Pipeline
|
|
5
|
+
*
|
|
6
|
+
* Converts captured HTML files into React/JSX components while
|
|
7
|
+
* preserving exact class names, styles, and structure.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Parses HTML using jsdom
|
|
11
|
+
* - Extracts component tree structure
|
|
12
|
+
* - Converts HTML attributes to JSX (class→className, for→htmlFor)
|
|
13
|
+
* - Preserves inline styles as objects
|
|
14
|
+
* - Identifies semantic component boundaries
|
|
15
|
+
* - Generates React component files
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node html-to-react.js --project <name> --page <page>
|
|
19
|
+
* node html-to-react.js --input ./html/page.html --output ./components
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { JSDOM } = require('jsdom');
|
|
25
|
+
|
|
26
|
+
// HTML attributes that need transformation for JSX
|
|
27
|
+
const JSX_ATTR_MAP = {
|
|
28
|
+
'class': 'className',
|
|
29
|
+
'for': 'htmlFor',
|
|
30
|
+
'tabindex': 'tabIndex',
|
|
31
|
+
'readonly': 'readOnly',
|
|
32
|
+
'maxlength': 'maxLength',
|
|
33
|
+
'minlength': 'minLength',
|
|
34
|
+
'cellpadding': 'cellPadding',
|
|
35
|
+
'cellspacing': 'cellSpacing',
|
|
36
|
+
'rowspan': 'rowSpan',
|
|
37
|
+
'colspan': 'colSpan',
|
|
38
|
+
'usemap': 'useMap',
|
|
39
|
+
'frameborder': 'frameBorder',
|
|
40
|
+
'contenteditable': 'contentEditable',
|
|
41
|
+
'crossorigin': 'crossOrigin',
|
|
42
|
+
'datetime': 'dateTime',
|
|
43
|
+
'enctype': 'encType',
|
|
44
|
+
'formaction': 'formAction',
|
|
45
|
+
'formenctype': 'formEncType',
|
|
46
|
+
'formmethod': 'formMethod',
|
|
47
|
+
'formnovalidate': 'formNoValidate',
|
|
48
|
+
'formtarget': 'formTarget',
|
|
49
|
+
'hreflang': 'hrefLang',
|
|
50
|
+
'inputmode': 'inputMode',
|
|
51
|
+
'srcdoc': 'srcDoc',
|
|
52
|
+
'srcset': 'srcSet',
|
|
53
|
+
'autocomplete': 'autoComplete',
|
|
54
|
+
'autofocus': 'autoFocus',
|
|
55
|
+
'autoplay': 'autoPlay'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Self-closing tags in JSX
|
|
59
|
+
const SELF_CLOSING_TAGS = new Set([
|
|
60
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
61
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// Tags to skip (scripts, etc.)
|
|
65
|
+
const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'link', 'meta']);
|
|
66
|
+
|
|
67
|
+
class HTMLToReactConverter {
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
this.options = {
|
|
70
|
+
preserveComments: false,
|
|
71
|
+
generateTypeScript: true,
|
|
72
|
+
componentPrefix: '',
|
|
73
|
+
wrapWithFragment: true,
|
|
74
|
+
extractComponents: true,
|
|
75
|
+
minComponentDepth: 2,
|
|
76
|
+
...options
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this.dom = null;
|
|
80
|
+
this.document = null;
|
|
81
|
+
this.extractedComponents = [];
|
|
82
|
+
this.componentCounter = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load HTML from file
|
|
87
|
+
*/
|
|
88
|
+
loadFromFile(htmlPath) {
|
|
89
|
+
if (!fs.existsSync(htmlPath)) {
|
|
90
|
+
throw new Error(`HTML file not found: ${htmlPath}`);
|
|
91
|
+
}
|
|
92
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
93
|
+
return this.loadFromString(html);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Load HTML from string
|
|
98
|
+
*/
|
|
99
|
+
loadFromString(html) {
|
|
100
|
+
this.dom = new JSDOM(html);
|
|
101
|
+
this.document = this.dom.window.document;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse inline style string to object
|
|
107
|
+
*/
|
|
108
|
+
parseStyleString(styleStr) {
|
|
109
|
+
if (!styleStr) return null;
|
|
110
|
+
|
|
111
|
+
const styleObj = {};
|
|
112
|
+
const rules = styleStr.split(';').filter(Boolean);
|
|
113
|
+
|
|
114
|
+
for (const rule of rules) {
|
|
115
|
+
const [property, value] = rule.split(':').map(s => s.trim());
|
|
116
|
+
if (property && value) {
|
|
117
|
+
// Convert CSS property to camelCase
|
|
118
|
+
const camelProperty = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
119
|
+
styleObj[camelProperty] = value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return Object.keys(styleObj).length > 0 ? styleObj : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format style object as JSX string
|
|
128
|
+
*/
|
|
129
|
+
formatStyleObject(styleObj) {
|
|
130
|
+
if (!styleObj) return null;
|
|
131
|
+
|
|
132
|
+
const entries = Object.entries(styleObj)
|
|
133
|
+
.map(([key, value]) => {
|
|
134
|
+
// Wrap string values in quotes, keep numbers as-is
|
|
135
|
+
const formattedValue = typeof value === 'number' ? value : `"${value}"`;
|
|
136
|
+
return `${key}: ${formattedValue}`;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return `{{ ${entries.join(', ')} }}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert HTML element to JSX string
|
|
144
|
+
*/
|
|
145
|
+
elementToJSX(element, indent = 0) {
|
|
146
|
+
const indentStr = ' '.repeat(indent);
|
|
147
|
+
|
|
148
|
+
// Handle text nodes
|
|
149
|
+
if (element.nodeType === 3) { // TEXT_NODE
|
|
150
|
+
const text = element.textContent.trim();
|
|
151
|
+
if (!text) return '';
|
|
152
|
+
// Escape JSX special characters
|
|
153
|
+
return text
|
|
154
|
+
.replace(/\{/g, '{')
|
|
155
|
+
.replace(/\}/g, '}');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle comment nodes
|
|
159
|
+
if (element.nodeType === 8) { // COMMENT_NODE
|
|
160
|
+
if (!this.options.preserveComments) return '';
|
|
161
|
+
return `${indentStr}{/* ${element.textContent.trim()} */}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Skip non-element nodes
|
|
165
|
+
if (element.nodeType !== 1) return ''; // ELEMENT_NODE
|
|
166
|
+
|
|
167
|
+
const tagName = element.tagName.toLowerCase();
|
|
168
|
+
|
|
169
|
+
// Skip certain tags
|
|
170
|
+
if (SKIP_TAGS.has(tagName)) return '';
|
|
171
|
+
|
|
172
|
+
// Build attributes
|
|
173
|
+
const attrs = this.buildAttributes(element);
|
|
174
|
+
const attrsStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
175
|
+
|
|
176
|
+
// Handle self-closing tags
|
|
177
|
+
if (SELF_CLOSING_TAGS.has(tagName)) {
|
|
178
|
+
return `${indentStr}<${tagName}${attrsStr} />`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Handle children
|
|
182
|
+
const children = Array.from(element.childNodes)
|
|
183
|
+
.map(child => this.elementToJSX(child, indent + 1))
|
|
184
|
+
.filter(Boolean);
|
|
185
|
+
|
|
186
|
+
// If no children
|
|
187
|
+
if (children.length === 0) {
|
|
188
|
+
// Check for text content
|
|
189
|
+
const textContent = element.textContent.trim();
|
|
190
|
+
if (textContent) {
|
|
191
|
+
return `${indentStr}<${tagName}${attrsStr}>${textContent}</${tagName}>`;
|
|
192
|
+
}
|
|
193
|
+
return `${indentStr}<${tagName}${attrsStr} />`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If single text child
|
|
197
|
+
if (children.length === 1 && !children[0].includes('\n') && children[0].length < 60) {
|
|
198
|
+
return `${indentStr}<${tagName}${attrsStr}>${children[0].trim()}</${tagName}>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Multi-line
|
|
202
|
+
return [
|
|
203
|
+
`${indentStr}<${tagName}${attrsStr}>`,
|
|
204
|
+
...children,
|
|
205
|
+
`${indentStr}</${tagName}>`
|
|
206
|
+
].join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build JSX attributes from element
|
|
211
|
+
*/
|
|
212
|
+
buildAttributes(element) {
|
|
213
|
+
const attrs = [];
|
|
214
|
+
|
|
215
|
+
for (const attr of element.attributes) {
|
|
216
|
+
let name = attr.name.toLowerCase();
|
|
217
|
+
let value = attr.value;
|
|
218
|
+
|
|
219
|
+
// Skip event handlers
|
|
220
|
+
if (name.startsWith('on')) continue;
|
|
221
|
+
|
|
222
|
+
// Skip data attributes that look like framework-specific
|
|
223
|
+
if (name.startsWith('data-react') || name.startsWith('data-v-')) continue;
|
|
224
|
+
|
|
225
|
+
// Transform attribute name
|
|
226
|
+
name = JSX_ATTR_MAP[name] || name;
|
|
227
|
+
|
|
228
|
+
// Handle special attributes
|
|
229
|
+
if (name === 'style') {
|
|
230
|
+
const styleObj = this.parseStyleString(value);
|
|
231
|
+
if (styleObj) {
|
|
232
|
+
attrs.push(`style={${JSON.stringify(styleObj)}}`);
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle boolean attributes
|
|
238
|
+
if (value === '' || value === name || value === 'true') {
|
|
239
|
+
attrs.push(name);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Handle className (might have dynamic parts)
|
|
244
|
+
if (name === 'className') {
|
|
245
|
+
attrs.push(`className="${value}"`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Regular attribute
|
|
250
|
+
attrs.push(`${name}="${value}"`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return attrs;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract semantic component boundaries
|
|
258
|
+
*/
|
|
259
|
+
extractComponentBoundaries() {
|
|
260
|
+
const boundaries = [];
|
|
261
|
+
|
|
262
|
+
// Look for semantic elements
|
|
263
|
+
const semanticSelectors = [
|
|
264
|
+
'header', 'nav', 'main', 'aside', 'footer', 'article', 'section',
|
|
265
|
+
'[role="banner"]', '[role="navigation"]', '[role="main"]',
|
|
266
|
+
'[role="complementary"]', '[role="contentinfo"]'
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const selector of semanticSelectors) {
|
|
270
|
+
const elements = this.document.querySelectorAll(selector);
|
|
271
|
+
for (const el of elements) {
|
|
272
|
+
boundaries.push({
|
|
273
|
+
type: el.tagName.toLowerCase(),
|
|
274
|
+
role: el.getAttribute('role'),
|
|
275
|
+
id: el.id,
|
|
276
|
+
className: el.className,
|
|
277
|
+
element: el,
|
|
278
|
+
suggestedName: this.suggestComponentName(el)
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Also look for common class patterns
|
|
284
|
+
const classPatterns = [
|
|
285
|
+
{ pattern: /header/i, name: 'Header' },
|
|
286
|
+
{ pattern: /nav(bar|igation)?/i, name: 'Navigation' },
|
|
287
|
+
{ pattern: /sidebar/i, name: 'Sidebar' },
|
|
288
|
+
{ pattern: /footer/i, name: 'Footer' },
|
|
289
|
+
{ pattern: /card/i, name: 'Card' },
|
|
290
|
+
{ pattern: /modal/i, name: 'Modal' },
|
|
291
|
+
{ pattern: /table/i, name: 'DataTable' },
|
|
292
|
+
{ pattern: /form/i, name: 'Form' },
|
|
293
|
+
{ pattern: /button/i, name: 'Button' },
|
|
294
|
+
{ pattern: /panel/i, name: 'Panel' },
|
|
295
|
+
{ pattern: /container/i, name: 'Container' }
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
// Find elements by class patterns
|
|
299
|
+
const allElements = this.document.querySelectorAll('*[class]');
|
|
300
|
+
for (const el of allElements) {
|
|
301
|
+
const className = el.className;
|
|
302
|
+
if (typeof className !== 'string') continue;
|
|
303
|
+
|
|
304
|
+
for (const { pattern, name } of classPatterns) {
|
|
305
|
+
if (pattern.test(className)) {
|
|
306
|
+
// Avoid duplicates
|
|
307
|
+
const exists = boundaries.some(b =>
|
|
308
|
+
b.element === el || (b.className === className && b.type === el.tagName.toLowerCase())
|
|
309
|
+
);
|
|
310
|
+
if (!exists) {
|
|
311
|
+
boundaries.push({
|
|
312
|
+
type: el.tagName.toLowerCase(),
|
|
313
|
+
id: el.id,
|
|
314
|
+
className: className,
|
|
315
|
+
element: el,
|
|
316
|
+
suggestedName: name
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return boundaries;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Suggest component name from element
|
|
329
|
+
*/
|
|
330
|
+
suggestComponentName(element) {
|
|
331
|
+
// Try ID first
|
|
332
|
+
if (element.id) {
|
|
333
|
+
return this.toPascalCase(element.id);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Try main class
|
|
337
|
+
const className = element.className;
|
|
338
|
+
if (typeof className === 'string' && className) {
|
|
339
|
+
const mainClass = className.split(' ')[0];
|
|
340
|
+
return this.toPascalCase(mainClass);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Try tag name
|
|
344
|
+
const tagName = element.tagName.toLowerCase();
|
|
345
|
+
const semanticMap = {
|
|
346
|
+
'header': 'Header',
|
|
347
|
+
'nav': 'Navigation',
|
|
348
|
+
'main': 'MainContent',
|
|
349
|
+
'aside': 'Sidebar',
|
|
350
|
+
'footer': 'Footer',
|
|
351
|
+
'article': 'Article',
|
|
352
|
+
'section': 'Section'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return semanticMap[tagName] || `Component${++this.componentCounter}`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Convert string to PascalCase
|
|
360
|
+
*/
|
|
361
|
+
toPascalCase(str) {
|
|
362
|
+
return str
|
|
363
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '')
|
|
364
|
+
.replace(/^(.)/, c => c.toUpperCase())
|
|
365
|
+
.replace(/[^a-zA-Z0-9]/g, '');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Convert full document body to JSX
|
|
370
|
+
*/
|
|
371
|
+
convertBody() {
|
|
372
|
+
const body = this.document.body;
|
|
373
|
+
if (!body) return '';
|
|
374
|
+
|
|
375
|
+
return this.elementToJSX(body, 0);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Convert specific element to JSX
|
|
380
|
+
*/
|
|
381
|
+
convertElement(selector) {
|
|
382
|
+
const element = this.document.querySelector(selector);
|
|
383
|
+
if (!element) return null;
|
|
384
|
+
|
|
385
|
+
return this.elementToJSX(element, 0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Generate React component from element
|
|
390
|
+
*/
|
|
391
|
+
generateComponent(element, componentName) {
|
|
392
|
+
const jsx = this.elementToJSX(element, 2);
|
|
393
|
+
const ext = this.options.generateTypeScript ? 'tsx' : 'jsx';
|
|
394
|
+
const propsType = this.options.generateTypeScript ? ': React.FC' : '';
|
|
395
|
+
|
|
396
|
+
const component = `import React from 'react';
|
|
397
|
+
|
|
398
|
+
export const ${componentName}${propsType} = () => {
|
|
399
|
+
return (
|
|
400
|
+
${jsx}
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
export default ${componentName};
|
|
405
|
+
`;
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
name: componentName,
|
|
409
|
+
filename: `${componentName}.${ext}`,
|
|
410
|
+
content: component,
|
|
411
|
+
jsx: jsx
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Extract and generate all components
|
|
417
|
+
*/
|
|
418
|
+
extractAllComponents() {
|
|
419
|
+
const boundaries = this.extractComponentBoundaries();
|
|
420
|
+
const components = [];
|
|
421
|
+
|
|
422
|
+
for (const boundary of boundaries) {
|
|
423
|
+
if (!boundary.element) continue;
|
|
424
|
+
|
|
425
|
+
const componentName = boundary.suggestedName;
|
|
426
|
+
const component = this.generateComponent(boundary.element, componentName);
|
|
427
|
+
components.push(component);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return components;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generate page component that imports all extracted components
|
|
435
|
+
*/
|
|
436
|
+
generatePageComponent(pageName, components) {
|
|
437
|
+
const ext = this.options.generateTypeScript ? 'tsx' : 'jsx';
|
|
438
|
+
const propsType = this.options.generateTypeScript ? ': React.FC' : '';
|
|
439
|
+
|
|
440
|
+
const imports = components
|
|
441
|
+
.map(c => `import { ${c.name} } from './${c.name}';`)
|
|
442
|
+
.join('\n');
|
|
443
|
+
|
|
444
|
+
const componentUsage = components
|
|
445
|
+
.map(c => ` <${c.name} />`)
|
|
446
|
+
.join('\n');
|
|
447
|
+
|
|
448
|
+
const page = `import React from 'react';
|
|
449
|
+
${imports}
|
|
450
|
+
|
|
451
|
+
export const ${pageName}${propsType} = () => {
|
|
452
|
+
return (
|
|
453
|
+
<div>
|
|
454
|
+
${componentUsage}
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export default ${pageName};
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
name: pageName,
|
|
464
|
+
filename: `${pageName}.${ext}`,
|
|
465
|
+
content: page
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get component tree structure
|
|
471
|
+
*/
|
|
472
|
+
getComponentTree() {
|
|
473
|
+
const body = this.document.body;
|
|
474
|
+
if (!body) return null;
|
|
475
|
+
|
|
476
|
+
const buildTree = (element, depth = 0) => {
|
|
477
|
+
if (element.nodeType !== 1) return null;
|
|
478
|
+
|
|
479
|
+
const tagName = element.tagName.toLowerCase();
|
|
480
|
+
if (SKIP_TAGS.has(tagName)) return null;
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
tag: tagName,
|
|
484
|
+
id: element.id || null,
|
|
485
|
+
className: typeof element.className === 'string' ? element.className : null,
|
|
486
|
+
children: Array.from(element.children)
|
|
487
|
+
.map(child => buildTree(child, depth + 1))
|
|
488
|
+
.filter(Boolean)
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
return buildTree(body);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Convert HTML file to React components
|
|
498
|
+
*/
|
|
499
|
+
function convertHTMLToReact(htmlPath, options = {}) {
|
|
500
|
+
const converter = new HTMLToReactConverter(options);
|
|
501
|
+
converter.loadFromFile(htmlPath);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
fullJSX: converter.convertBody(),
|
|
505
|
+
components: converter.extractAllComponents(),
|
|
506
|
+
tree: converter.getComponentTree(),
|
|
507
|
+
boundaries: converter.extractComponentBoundaries()
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Write components to output directory
|
|
513
|
+
*/
|
|
514
|
+
function writeComponents(components, outputDir) {
|
|
515
|
+
if (!fs.existsSync(outputDir)) {
|
|
516
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const written = [];
|
|
520
|
+
|
|
521
|
+
for (const component of components) {
|
|
522
|
+
const filePath = path.join(outputDir, component.filename);
|
|
523
|
+
fs.writeFileSync(filePath, component.content);
|
|
524
|
+
written.push(filePath);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return written;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// CLI execution
|
|
531
|
+
if (require.main === module) {
|
|
532
|
+
const args = process.argv.slice(2);
|
|
533
|
+
let inputPath = null;
|
|
534
|
+
let outputDir = null;
|
|
535
|
+
let projectName = null;
|
|
536
|
+
let pageName = null;
|
|
537
|
+
let showTree = false;
|
|
538
|
+
|
|
539
|
+
for (let i = 0; i < args.length; i++) {
|
|
540
|
+
switch (args[i]) {
|
|
541
|
+
case '--input':
|
|
542
|
+
case '-i':
|
|
543
|
+
inputPath = args[++i];
|
|
544
|
+
break;
|
|
545
|
+
case '--output':
|
|
546
|
+
case '-o':
|
|
547
|
+
outputDir = args[++i];
|
|
548
|
+
break;
|
|
549
|
+
case '--project':
|
|
550
|
+
projectName = args[++i];
|
|
551
|
+
break;
|
|
552
|
+
case '--page':
|
|
553
|
+
pageName = args[++i];
|
|
554
|
+
break;
|
|
555
|
+
case '--tree':
|
|
556
|
+
showTree = true;
|
|
557
|
+
break;
|
|
558
|
+
case '--help':
|
|
559
|
+
case '-h':
|
|
560
|
+
console.log(`
|
|
561
|
+
Usage: node html-to-react.js [options]
|
|
562
|
+
|
|
563
|
+
Options:
|
|
564
|
+
--input, -i <path> Path to HTML file
|
|
565
|
+
--output, -o <path> Output directory for components
|
|
566
|
+
--project <name> Project name
|
|
567
|
+
--page <name> Page name (used with --project)
|
|
568
|
+
--tree Show component tree only
|
|
569
|
+
--help, -h Show this help
|
|
570
|
+
|
|
571
|
+
Examples:
|
|
572
|
+
node html-to-react.js --input ./html/page.html --output ./components
|
|
573
|
+
node html-to-react.js --project my-app --page account-detail
|
|
574
|
+
node html-to-react.js --input ./page.html --tree
|
|
575
|
+
`);
|
|
576
|
+
process.exit(0);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Handle project-based paths
|
|
581
|
+
if (projectName) {
|
|
582
|
+
const SKILL_DIR = path.dirname(__dirname);
|
|
583
|
+
const PROJECTS_DIR = path.resolve(SKILL_DIR, '../../../projects');
|
|
584
|
+
const projectDir = path.join(PROJECTS_DIR, projectName);
|
|
585
|
+
|
|
586
|
+
if (pageName) {
|
|
587
|
+
inputPath = path.join(projectDir, 'references', 'html', `${pageName}.html`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
outputDir = outputDir || path.join(projectDir, 'prototype', 'src', 'components', 'extracted');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!inputPath) {
|
|
594
|
+
console.error('\x1b[31mError:\x1b[0m --input is required');
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
console.log(`\n\x1b[1mHTML to React Converter\x1b[0m`);
|
|
600
|
+
console.log(`Input: ${inputPath}`);
|
|
601
|
+
|
|
602
|
+
const result = convertHTMLToReact(inputPath);
|
|
603
|
+
|
|
604
|
+
if (showTree) {
|
|
605
|
+
console.log(`\n\x1b[1mComponent Tree:\x1b[0m`);
|
|
606
|
+
console.log(JSON.stringify(result.tree, null, 2));
|
|
607
|
+
process.exit(0);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
console.log(`\n\x1b[1mDetected Component Boundaries (${result.boundaries.length}):\x1b[0m`);
|
|
611
|
+
for (const boundary of result.boundaries.slice(0, 10)) {
|
|
612
|
+
console.log(` ${boundary.suggestedName} (${boundary.type}${boundary.className ? '.' + boundary.className.split(' ')[0] : ''})`);
|
|
613
|
+
}
|
|
614
|
+
if (result.boundaries.length > 10) {
|
|
615
|
+
console.log(` ... and ${result.boundaries.length - 10} more`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
console.log(`\n\x1b[1mExtracted Components (${result.components.length}):\x1b[0m`);
|
|
619
|
+
for (const component of result.components) {
|
|
620
|
+
console.log(` ${component.name} (${component.filename})`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (outputDir) {
|
|
624
|
+
console.log(`\n\x1b[1mWriting to:\x1b[0m ${outputDir}`);
|
|
625
|
+
const written = writeComponents(result.components, outputDir);
|
|
626
|
+
console.log(`\x1b[32m✓ Wrote ${written.length} component files\x1b[0m`);
|
|
627
|
+
|
|
628
|
+
for (const file of written) {
|
|
629
|
+
console.log(` ${path.basename(file)}`);
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
console.log(`\n\x1b[33mTip:\x1b[0m Use --output <dir> to write component files`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
module.exports = {
|
|
642
|
+
HTMLToReactConverter,
|
|
643
|
+
convertHTMLToReact,
|
|
644
|
+
writeComponents
|
|
645
|
+
};
|