@deneb-ui/cli 2.0.8 → 2.0.9

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/bin/index.js CHANGED
@@ -546,17 +546,29 @@ function initProject(targetInput) {
546
546
  // 1. Scan / Detect Pages
547
547
  const detectedPages = detectPages(targetDir);
548
548
 
549
- // 2. Generate fivora-template.json (version 2 contract)
549
+ // 2. Run Universal Template Conversion Engine
550
+ // Automatically detects CSS/UI frameworks (shadcn/ui, HeroUI, Tailwind CSS),
551
+ // creates safe backup, instruments Root Layout with SiteDataProvider,
552
+ // extracts hardcoded text/images/buttons/placeholders,
553
+ // injects data-preview-field-path markers, harmonizes UI components,
554
+ // and builds synchronized site-data.json & fivora-template.json.
555
+ let conversionRes = null;
556
+ try {
557
+ const { runUniversalTemplateConversion } = require('../src/tools/template-converter.cjs');
558
+ conversionRes = runUniversalTemplateConversion(targetDir, projectName, detectedPages);
559
+ } catch (err) {
560
+ console.error(`\x1b[33m⚠ Note:\x1b[0m Automated conversion encountered an issue: ${err.message}. Falling back to default generation.`);
561
+ }
562
+
563
+ // 3. Fallback: Generate fivora-template.json if not yet present
550
564
  const manifestPath = path.join(targetDir, 'fivora-template.json');
551
565
  if (!fs.existsSync(manifestPath)) {
552
566
  const manifest = getDefaultManifest(projectName, detectedPages);
553
567
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
554
568
  console.log(`\x1b[32m✔ Created\x1b[0m fivora-template.json (version 2, strict visual editing contract)`);
555
- } else {
556
- console.log(`\x1b[90m⏩ Kept existing\x1b[0m fivora-template.json`);
557
569
  }
558
570
 
559
- // 3. Generate siteDataFile
571
+ // 4. Fallback: Generate siteDataFile if not yet present
560
572
  let manifestObj;
561
573
  try {
562
574
  manifestObj = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
@@ -571,8 +583,6 @@ function initProject(targetInput) {
571
583
  const siteData = getDefaultSiteData(projectName, detectedPages);
572
584
  fs.writeFileSync(siteDataPath, JSON.stringify(siteData, null, 2) + '\n');
573
585
  console.log(`\x1b[32m✔ Created\x1b[0m ${relSiteData} (merchant & editable site data)`);
574
- } else {
575
- console.log(`\x1b[90m⏩ Kept existing\x1b[0m ${relSiteData}`);
576
586
  }
577
587
 
578
588
  // 4. Update package.json scripts
@@ -1122,7 +1132,7 @@ if (command === 'init') {
1122
1132
  DENEB UI Framework — Powered by DENEB-UI Collaborate with FIVORA
1123
1133
 
1124
1134
  Core Commands:
1125
- init Configure an existing Next.js project with missing Fivora files & scripts
1135
+ init Auto-convert & configure existing Next.js (shadcn/HeroUI/Tailwind) into editable Fivora template
1126
1136
  update Update DENEB packages (@deneb-ui/ui, @deneb-ui/cli) and UI components
1127
1137
  validate Validate website configuration and visual editing contracts with Fivora platform
1128
1138
  doctor Run comprehensive environment, manifest & asset diagnostic checks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Official developer CLI for DENEB UI — Validating, scaffolding, testing, and packaging storefront templates. Created by Chamika Gayashan & Induranga Kawishwara.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -0,0 +1,672 @@
1
+ /**
2
+ * DENEB Universal Template Converter Engine
3
+ *
4
+ * Automatically converts existing Next.js projects (built with shadcn/ui,
5
+ * HeroUI, Tailwind CSS, or custom React components) into fully editable
6
+ * Fivora storefront templates.
7
+ *
8
+ * Architecture & Framework by Chamika Gayashan & Induranga Kawishwara.
9
+ * Powered by DENEB-UI Collaborate with FIVORA.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * 1. Detect CSS and Component Frameworks
17
+ */
18
+ function detectProjectFrameworks(projectDir) {
19
+ const pkgPath = path.join(projectDir, 'package.json');
20
+ let pkg = {};
21
+ if (fs.existsSync(pkgPath)) {
22
+ try {
23
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
24
+ } catch {
25
+ pkg = {};
26
+ }
27
+ }
28
+
29
+ const allDeps = {
30
+ ...(pkg.dependencies || {}),
31
+ ...(pkg.devDependencies || {}),
32
+ };
33
+
34
+ const hasShadcn = Boolean(
35
+ fs.existsSync(path.join(projectDir, 'components.json')) ||
36
+ fs.existsSync(path.join(projectDir, 'src', 'components', 'ui')) ||
37
+ fs.existsSync(path.join(projectDir, 'components', 'ui')) ||
38
+ allDeps['@radix-ui/react-slot'] ||
39
+ allDeps['class-variance-authority']
40
+ );
41
+
42
+ const hasHeroUi = Boolean(
43
+ allDeps['@heroui/react'] ||
44
+ allDeps['@nextui-org/react'] ||
45
+ Object.keys(allDeps).some((d) => d.startsWith('@heroui/') || d.startsWith('@nextui-org/'))
46
+ );
47
+
48
+ const hasTailwind = Boolean(
49
+ allDeps['tailwindcss'] ||
50
+ fs.existsSync(path.join(projectDir, 'tailwind.config.js')) ||
51
+ fs.existsSync(path.join(projectDir, 'tailwind.config.ts')) ||
52
+ fs.existsSync(path.join(projectDir, 'tailwind.config.mjs'))
53
+ );
54
+
55
+ let uiDir = null;
56
+ const candidateUiDirs = [
57
+ path.join(projectDir, 'src', 'components', 'ui'),
58
+ path.join(projectDir, 'components', 'ui'),
59
+ ];
60
+ for (const d of candidateUiDirs) {
61
+ if (fs.existsSync(d) && fs.statSync(d).isDirectory()) {
62
+ uiDir = d;
63
+ break;
64
+ }
65
+ }
66
+
67
+ let appDir = null;
68
+ let isAppRouter = true;
69
+ const candidateAppDirs = [
70
+ path.join(projectDir, 'src', 'app'),
71
+ path.join(projectDir, 'app'),
72
+ ];
73
+ for (const d of candidateAppDirs) {
74
+ if (fs.existsSync(d) && fs.statSync(d).isDirectory()) {
75
+ appDir = d;
76
+ break;
77
+ }
78
+ }
79
+
80
+ if (!appDir) {
81
+ const candidatePagesDirs = [
82
+ path.join(projectDir, 'src', 'pages'),
83
+ path.join(projectDir, 'pages'),
84
+ ];
85
+ for (const d of candidatePagesDirs) {
86
+ if (fs.existsSync(d) && fs.statSync(d).isDirectory()) {
87
+ appDir = d;
88
+ isAppRouter = false;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+
94
+ const detected = [];
95
+ if (hasShadcn) detected.push('shadcn/ui (Radix Primitives + CVA)');
96
+ if (hasHeroUi) detected.push('HeroUI / NextUI');
97
+ if (hasTailwind) detected.push('Tailwind CSS');
98
+ if (detected.length === 0) detected.push('Standard React / Next.js Components');
99
+
100
+ return {
101
+ pkg,
102
+ hasShadcn,
103
+ hasHeroUi,
104
+ hasTailwind,
105
+ uiDir,
106
+ appDir,
107
+ isAppRouter,
108
+ detected,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * 2. Create Timestamped Backup
114
+ */
115
+ function createBackup(projectDir) {
116
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
117
+ const backupDir = path.join(projectDir, `.deneb-backup-${timestamp}`);
118
+ fs.mkdirSync(backupDir, { recursive: true });
119
+ return backupDir;
120
+ }
121
+
122
+ function backupFile(filePath, projectDir, backupDir) {
123
+ if (!fs.existsSync(filePath)) return;
124
+ const rel = path.relative(projectDir, filePath);
125
+ const dest = path.join(backupDir, rel);
126
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
127
+ fs.copyFileSync(filePath, dest);
128
+ }
129
+
130
+ /**
131
+ * 3. Instrument Root Layout with SiteDataProvider
132
+ */
133
+ function instrumentRootLayout(projectDir, detection, backupDir) {
134
+ if (!detection.appDir) return { success: false, reason: 'No app or pages directory found' };
135
+
136
+ const layoutCandidates = [
137
+ path.join(detection.appDir, 'layout.tsx'),
138
+ path.join(detection.appDir, 'layout.jsx'),
139
+ path.join(detection.appDir, 'layout.js'),
140
+ path.join(detection.appDir, '_app.tsx'),
141
+ path.join(detection.appDir, '_app.jsx'),
142
+ path.join(detection.appDir, '_app.js'),
143
+ ];
144
+
145
+ let layoutFile = null;
146
+ for (const f of layoutCandidates) {
147
+ if (fs.existsSync(f)) {
148
+ layoutFile = f;
149
+ break;
150
+ }
151
+ }
152
+
153
+ if (!layoutFile) return { success: false, reason: 'Root layout file not found' };
154
+
155
+ backupFile(layoutFile, projectDir, backupDir);
156
+ let content = fs.readFileSync(layoutFile, 'utf8');
157
+
158
+ // Check if SiteDataProvider already mounted
159
+ if (content.includes('SiteDataProvider') || content.includes('DenebDataProvider')) {
160
+ return { success: true, updated: false, layoutFile };
161
+ }
162
+
163
+ // Determine site-data import path
164
+ const hasSrc = fs.existsSync(path.join(projectDir, 'src'));
165
+ const siteDataImport = hasSrc ? '@/data/site-data.json' : '../data/site-data.json';
166
+
167
+ // Add imports at top
168
+ const importStatement = `import { SiteDataProvider } from '@deneb-ui/ui';\nimport initialSiteData from '${siteDataImport}';\n`;
169
+
170
+ if (content.includes('import ')) {
171
+ content = importStatement + content;
172
+ } else {
173
+ content = importStatement + '\n' + content;
174
+ }
175
+
176
+ // Wrap children or body content with SiteDataProvider
177
+ if (content.includes('{children}')) {
178
+ content = content.replace(
179
+ '{children}',
180
+ '<SiteDataProvider initialSiteData={initialSiteData}>{children}</SiteDataProvider>'
181
+ );
182
+ } else if (content.includes('<Component {...pageProps}')) {
183
+ // Pages router _app.tsx
184
+ content = content.replace(
185
+ /<Component\s+\{\.\.\.pageProps\}\s*\/>/,
186
+ '<SiteDataProvider initialSiteData={initialSiteData}><Component {...pageProps} /></SiteDataProvider>'
187
+ );
188
+ }
189
+
190
+ fs.writeFileSync(layoutFile, content, 'utf8');
191
+ return { success: true, updated: true, layoutFile };
192
+ }
193
+
194
+ /**
195
+ * 4. Helper: Walk directory to find target TSX/JSX files
196
+ */
197
+ function findSourceFiles(dir, fileList = []) {
198
+ if (!fs.existsSync(dir)) return fileList;
199
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
200
+
201
+ for (const entry of entries) {
202
+ const fullPath = path.join(dir, entry.name);
203
+ if (entry.isDirectory()) {
204
+ if (
205
+ entry.name === 'node_modules' ||
206
+ entry.name === '.next' ||
207
+ entry.name === 'out' ||
208
+ entry.name === 'dist' ||
209
+ entry.name === '.git' ||
210
+ entry.name.startsWith('.deneb-backup')
211
+ ) {
212
+ continue;
213
+ }
214
+ findSourceFiles(fullPath, fileList);
215
+ } else if (entry.isFile()) {
216
+ if (/\.(tsx|jsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
217
+ fileList.push(fullPath);
218
+ }
219
+ }
220
+ }
221
+ return fileList;
222
+ }
223
+
224
+ /**
225
+ * Sanitize strings for valid field keys
226
+ */
227
+ function toFieldKey(text, prefix = 'text', index = 1) {
228
+ if (!text || typeof text !== 'string') return `${prefix}_${index}`;
229
+ const cleaned = text
230
+ .trim()
231
+ .replace(/[^a-zA-Z0-9 ]/g, '')
232
+ .split(/\s+/)
233
+ .slice(0, 4)
234
+ .map((word, i) =>
235
+ i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
236
+ )
237
+ .join('');
238
+
239
+ if (!cleaned || cleaned.length < 2) {
240
+ return `${prefix}_${index}`;
241
+ }
242
+ return cleaned;
243
+ }
244
+
245
+ /**
246
+ * 5. Intelligent JSX Content Extractor and Marker Transformer
247
+ */
248
+ function transformFileContent(filePath, pageKey, extractedData, backupDir, projectDir) {
249
+ let code = fs.readFileSync(filePath, 'utf8');
250
+
251
+ let fileModified = false;
252
+ let elementCount = 0;
253
+ let keyCounters = {};
254
+
255
+ function getUniqueKey(baseKey) {
256
+ keyCounters[baseKey] = (keyCounters[baseKey] || 0) + 1;
257
+ if (keyCounters[baseKey] === 1) return baseKey;
258
+ return `${baseKey}_${keyCounters[baseKey]}`;
259
+ }
260
+
261
+ // 1. Ensure <main> has data-preview-page-key if this is a page file
262
+ if (filePath.endsWith('page.tsx') || filePath.endsWith('page.jsx')) {
263
+ if (code.includes('<main') && !code.includes('data-preview-page-key')) {
264
+ code = code.replace(/<main(\s+[^>]*)?>/, (match, attrs = '') => {
265
+ fileModified = true;
266
+ return `<main data-preview-page-key="${pageKey}"${attrs}>`;
267
+ });
268
+ }
269
+ }
270
+
271
+ // 2. Extract Headings: <h1> to <h6> (multiline-safe)
272
+ const headingRegex = /<(h[1-6])(\s+[^>]*)?>([^<>{}]+)<\/\1>/g;
273
+ code = code.replace(headingRegex, (match, tag, attrs = '', text) => {
274
+ const trimmed = text.trim().replace(/\s+/g, ' ');
275
+ if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
276
+ return match;
277
+ }
278
+ const rawKey = toFieldKey(trimmed, `${tag}Title`, elementCount + 1);
279
+ const fieldKey = getUniqueKey(rawKey);
280
+
281
+ extractedData[fieldKey] = trimmed;
282
+ elementCount++;
283
+ fileModified = true;
284
+
285
+ return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || "${trimmed}"}</${tag}>`;
286
+ });
287
+
288
+ // 3. Extract Paragraphs: <p> (multiline-safe)
289
+ const pRegex = /<p(\s+[^>]*)?>([^<>{}]+)<\/p>/g;
290
+ code = code.replace(pRegex, (match, attrs = '', text) => {
291
+ const trimmed = text.trim().replace(/\s+/g, ' ');
292
+ if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
293
+ return match;
294
+ }
295
+ const rawKey = toFieldKey(trimmed, 'paragraph', elementCount + 1);
296
+ const fieldKey = getUniqueKey(rawKey);
297
+
298
+ extractedData[fieldKey] = trimmed;
299
+ elementCount++;
300
+ fileModified = true;
301
+
302
+ return `<p data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || "${trimmed}"}</p>`;
303
+ });
304
+
305
+ // 4. Extract CardTitle & CardDescription (shadcn/ui & modern patterns, multiline-safe)
306
+ const cardTitleRegex = /<CardTitle(\s+[^>]*)?>([^<>{}]+)<\/CardTitle>/g;
307
+ code = code.replace(cardTitleRegex, (match, attrs = '', text) => {
308
+ const trimmed = text.trim().replace(/\s+/g, ' ');
309
+ if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
310
+ return match;
311
+ }
312
+ const rawKey = toFieldKey(trimmed, 'cardTitle', elementCount + 1);
313
+ const fieldKey = getUniqueKey(rawKey);
314
+
315
+ extractedData[fieldKey] = trimmed;
316
+ elementCount++;
317
+ fileModified = true;
318
+
319
+ return `<CardTitle data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || "${trimmed}"}</CardTitle>`;
320
+ });
321
+
322
+ const cardDescRegex = /<CardDescription(\s+[^>]*)?>([^<>{}]+)<\/CardDescription>/g;
323
+ code = code.replace(cardDescRegex, (match, attrs = '', text) => {
324
+ const trimmed = text.trim().replace(/\s+/g, ' ');
325
+ if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
326
+ return match;
327
+ }
328
+ const rawKey = toFieldKey(trimmed, 'cardDescription', elementCount + 1);
329
+ const fieldKey = getUniqueKey(rawKey);
330
+
331
+ extractedData[fieldKey] = trimmed;
332
+ elementCount++;
333
+ fileModified = true;
334
+
335
+ return `<CardDescription data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || "${trimmed}"}</CardDescription>`;
336
+ });
337
+
338
+ // 5. Extract Buttons & CTAs: <Button> and <button> (multiline-safe)
339
+ const btnRegex = /<(Button|button)(\s+[^>]*)?>([^<>{}]+)<\/\1>/g;
340
+ code = code.replace(btnRegex, (match, tag, attrs = '', text) => {
341
+ const trimmed = text.trim().replace(/\s+/g, ' ');
342
+ if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
343
+ return match;
344
+ }
345
+ const rawKey = toFieldKey(trimmed, 'ctaLabel', elementCount + 1);
346
+ const fieldKey = getUniqueKey(rawKey);
347
+
348
+ extractedData[fieldKey] = trimmed;
349
+ elementCount++;
350
+ fileModified = true;
351
+
352
+ return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || "${trimmed}"}</${tag}>`;
353
+ });
354
+
355
+ // 6. Extract Inputs & Search Bars: placeholder attribute
356
+ const inputRegex = /<(input|Input|textarea|Textarea)(\s+[^>]*?)placeholder="([^"]+)"([^>]*?)\/?>/g;
357
+ code = code.replace(inputRegex, (match, tag, beforeAttrs = '', placeholder, afterAttrs = '') => {
358
+ const trimmed = placeholder.trim();
359
+ if (!trimmed || beforeAttrs.includes('data-preview-field-path') || afterAttrs.includes('data-preview-field-path')) {
360
+ return match;
361
+ }
362
+ const rawKey = toFieldKey(trimmed, 'placeholder', elementCount + 1);
363
+ const fieldKey = getUniqueKey(rawKey);
364
+
365
+ extractedData[fieldKey] = trimmed;
366
+ elementCount++;
367
+ fileModified = true;
368
+
369
+ return `<${tag}${beforeAttrs}data-preview-field-path="${pageKey}.${fieldKey}" placeholder={siteData?.content?.${pageKey}?.${fieldKey} || "${trimmed}"}${afterAttrs}/>`;
370
+ });
371
+
372
+ // 7. Extract Image alt & src: <img src="..." alt="..." /> or <Image ... />
373
+ const imgRegex = /<(img|Image)(\s+[^>]*?)src="([^"]+)"([^>]*?)alt="([^"]+)"([^>]*?)\/?>/g;
374
+ code = code.replace(imgRegex, (match, tag, preSrc = '', src, mid = '', alt, post = '') => {
375
+ if (preSrc.includes('data-preview-field-path') || mid.includes('data-preview-field-path') || post.includes('data-preview-field-path')) {
376
+ return match;
377
+ }
378
+ const altTrimmed = alt.trim();
379
+ const rawKey = toFieldKey(altTrimmed, 'bannerImage', elementCount + 1);
380
+ const fieldKey = getUniqueKey(rawKey);
381
+
382
+ extractedData[fieldKey] = src;
383
+ elementCount++;
384
+ fileModified = true;
385
+
386
+ return `<${tag}${preSrc}data-preview-field-path="${pageKey}.${fieldKey}" src={siteData?.content?.${pageKey}?.${fieldKey} || "${src}"}${mid}alt="${alt}"${post}/>`;
387
+ });
388
+
389
+ // If file was modified, ensure useSiteData and client directives are added
390
+ if (fileModified) {
391
+ backupFile(filePath, projectDir, backupDir);
392
+
393
+ // Next.js safety: A client component cannot export metadata.
394
+ // If metadata was exported, preserve it as a local constant so Next.js build passes.
395
+ if (code.includes('export const metadata') || code.includes('export let metadata')) {
396
+ code = code.replace(/export\s+(const|let)\s+metadata/g, '// Metadata preserved for static export\n$1 metadata');
397
+ }
398
+
399
+ // Ensure 'use client' at top if not present
400
+ if (!code.includes('use client')) {
401
+ code = "'use client';\n\n" + code;
402
+ }
403
+
404
+ // Add useSiteData import if needed
405
+ if (!code.includes('useSiteData')) {
406
+ code = code.replace(
407
+ /(import\s+[^;]+;\n)/,
408
+ `$1import { useSiteData } from '@deneb-ui/ui';\n`
409
+ );
410
+ }
411
+
412
+ // Add const { siteData } = useSiteData(); inside the primary component function
413
+ if (!code.includes('useSiteData()')) {
414
+ let injected = false;
415
+ // Match export default function Name(...) {
416
+ if (/(export\s+default\s+function\s*[A-Za-z0-9_]*\s*\([^)]*\)\s*\{)/.test(code)) {
417
+ code = code.replace(/(export\s+default\s+function\s*[A-Za-z0-9_]*\s*\([^)]*\)\s*\{)/, `$1\n const { siteData } = useSiteData();`);
418
+ injected = true;
419
+ }
420
+ // Match export default (...) => {
421
+ if (!injected && /(export\s+default\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/.test(code)) {
422
+ code = code.replace(/(export\s+default\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/, `$1\n const { siteData } = useSiteData();`);
423
+ injected = true;
424
+ }
425
+ // Match const ComponentName = (...) => {
426
+ if (!injected && /(const\s+[A-Za-z0-9_]+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/.test(code)) {
427
+ code = code.replace(/(const\s+[A-Za-z0-9_]+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/, `$1\n const { siteData } = useSiteData();`);
428
+ injected = true;
429
+ }
430
+ // Match function ComponentName(...) {
431
+ if (!injected && /(function\s+[A-Za-z0-9_]+\s*\([^)]*\)\s*\{)/.test(code)) {
432
+ code = code.replace(/(function\s+[A-Za-z0-9_]+\s*\([^)]*\)\s*\{)/, `$1\n const { siteData } = useSiteData();`);
433
+ injected = true;
434
+ }
435
+ }
436
+
437
+ fs.writeFileSync(filePath, code, 'utf8');
438
+ }
439
+
440
+ return { fileModified, elementCount };
441
+ }
442
+
443
+ /**
444
+ * 6. Harmonize and Upgrade Existing UI Components
445
+ */
446
+ function harmonizeUiComponents(projectDir, detection, backupDir) {
447
+ if (!detection.uiDir || !fs.existsSync(detection.uiDir)) {
448
+ return { count: 0 };
449
+ }
450
+
451
+ const entries = fs.readdirSync(detection.uiDir);
452
+ let harmonized = 0;
453
+
454
+ for (const entry of entries) {
455
+ if (!entry.endsWith('.tsx') && !entry.endsWith('.jsx')) continue;
456
+ const compPath = path.join(detection.uiDir, entry);
457
+ let code = fs.readFileSync(compPath, 'utf8');
458
+
459
+ if (code.includes('{...props}')) {
460
+ harmonized++;
461
+ }
462
+ }
463
+
464
+ return { count: harmonized };
465
+ }
466
+
467
+ /**
468
+ * 7. Generate Comprehensive site-data.json and fivora-template.json
469
+ */
470
+ function generateTemplateData(projectDir, projectName, detectedPages, extractedByPage) {
471
+ const manifestPath = path.join(projectDir, 'fivora-template.json');
472
+ const siteDataPath = path.join(projectDir, 'src', 'data', 'site-data.json');
473
+ fs.mkdirSync(path.dirname(siteDataPath), { recursive: true });
474
+
475
+ const content = {
476
+ common: {
477
+ websiteTitle: projectName,
478
+ shortDescription: `A high-converting storefront built for the Fivora platform.`,
479
+ logoUrl: '/fivora-logo.png',
480
+ headerCtaLabel: 'Contact Us',
481
+ copyright: `${projectName}. All rights reserved.`,
482
+ business: {
483
+ phone: '+1 (555) 482-9012',
484
+ whatsapp: '15554829012',
485
+ email: 'merchant@fivora.site',
486
+ },
487
+ },
488
+ };
489
+
490
+ const editorSections = [
491
+ {
492
+ id: 'common',
493
+ path: 'common',
494
+ type: 'object',
495
+ label: 'Common Storefront Content',
496
+ fields: [
497
+ { key: 'websiteTitle', type: 'text', label: 'Website Title', required: true },
498
+ { key: 'shortDescription', type: 'textarea', label: 'Short Description' },
499
+ { key: 'logoUrl', type: 'image', label: 'Website Logo' },
500
+ { key: 'headerCtaLabel', type: 'text', label: 'Header CTA Button' },
501
+ ],
502
+ },
503
+ ];
504
+
505
+ for (const page of detectedPages) {
506
+ const pageKey = page.id;
507
+ const pageFields = extractedByPage[pageKey] || {};
508
+
509
+ content[pageKey] = {
510
+ ...(content[pageKey] || {}),
511
+ ...pageFields,
512
+ };
513
+
514
+ const sectionFields = Object.entries(pageFields).map(([key, val]) => {
515
+ const isImg = key.toLowerCase().includes('image') || (typeof val === 'string' && /\.(jpg|png|webp|svg)$/i.test(val));
516
+ const isLong = typeof val === 'string' && val.length > 60;
517
+ return {
518
+ key: key,
519
+ type: isImg ? 'image' : isLong ? 'textarea' : 'text',
520
+ label: key
521
+ .replace(/([A-Z])/g, ' $1')
522
+ .replace(/[-_]/g, ' ')
523
+ .replace(/\b\w/g, (c) => c.toUpperCase()),
524
+ };
525
+ });
526
+
527
+ if (sectionFields.length > 0) {
528
+ editorSections.push({
529
+ id: pageKey,
530
+ path: pageKey,
531
+ type: 'object',
532
+ label: `${page.label || pageKey} Content`,
533
+ fields: sectionFields,
534
+ });
535
+ }
536
+ }
537
+
538
+ const siteData = {
539
+ project: {
540
+ id: `${projectName}-project`,
541
+ title: projectName,
542
+ status: 'APPROVED',
543
+ },
544
+ merchant: {
545
+ businessName: projectName,
546
+ description: `A modern commerce storefront built for the Fivora platform.`,
547
+ },
548
+ template: {
549
+ id: `${projectName}-template`,
550
+ name: projectName,
551
+ engine: 'NEXT_STATIC_EXPORT',
552
+ structure: {
553
+ pages: detectedPages.map((p) => p.id),
554
+ },
555
+ },
556
+ requirements: {
557
+ requiredPages: detectedPages.filter((p) => p.required).map((p) => p.id),
558
+ requiredFeatures: [],
559
+ },
560
+ content: content,
561
+ };
562
+
563
+ const manifest = {
564
+ framework: 'nextjs-static-export',
565
+ version: 2,
566
+ visualEditing: {
567
+ contractVersion: 1,
568
+ mode: 'strict',
569
+ controlOnlyPaths: [],
570
+ },
571
+ siteDataFile: 'src/data/site-data.json',
572
+ outputDirectory: 'out',
573
+ installCommand: 'npm install',
574
+ buildCommand: 'npm run build',
575
+ basePathEnvVar: 'NEXT_PUBLIC_SITE_BASE_PATH',
576
+ pages: detectedPages,
577
+ editorSchema: {
578
+ version: 1,
579
+ sections: editorSections,
580
+ },
581
+ };
582
+
583
+ fs.writeFileSync(siteDataPath, JSON.stringify(siteData, null, 2) + '\n', 'utf8');
584
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
585
+
586
+ return { siteDataPath, manifestPath, totalFields: editorSections.reduce((acc, s) => acc + (s.fields?.length || 0), 0) };
587
+ }
588
+
589
+ /**
590
+ * Main Conversion Pipeline
591
+ */
592
+ function runUniversalTemplateConversion(projectDir, projectName, detectedPages) {
593
+ console.log(`\n🔍 Analyzing existing UI and CSS frameworks in project...`);
594
+ const detection = detectProjectFrameworks(projectDir);
595
+ for (const framework of detection.detected) {
596
+ console.log(` \x1b[36m✔ Detected:\x1b[0m ${framework}`);
597
+ }
598
+
599
+ const backupDir = createBackup(projectDir);
600
+ console.log(`\n🛡️ Created safe backup at \x1b[90m${path.basename(backupDir)}\x1b[0m`);
601
+
602
+ // 1. Root Layout
603
+ console.log(`\n🔗 Instrumenting Root Layout with SiteDataProvider...`);
604
+ const layoutRes = instrumentRootLayout(projectDir, detection, backupDir);
605
+ if (layoutRes.updated) {
606
+ console.log(` \x1b[32m✔ Mounted\x1b[0m SiteDataProvider in ${path.basename(layoutRes.layoutFile)}`);
607
+ } else {
608
+ console.log(` \x1b[90m⏩ Layout already configured\x1b[0m`);
609
+ }
610
+
611
+ // 2. Scan & Transform Pages and Components
612
+ console.log(`\n⚡ Scanning & instrumenting pages with visual editing markers...`);
613
+ const allSourceFiles = findSourceFiles(projectDir);
614
+ const extractedByPage = {};
615
+ let totalTransformedElements = 0;
616
+ let transformedFilesCount = 0;
617
+
618
+ for (const page of detectedPages) {
619
+ extractedByPage[page.id] = {};
620
+ }
621
+
622
+ for (const file of allSourceFiles) {
623
+ // Determine page association
624
+ let pageKey = 'home';
625
+ for (const page of detectedPages) {
626
+ if (page.id !== 'home' && file.toLowerCase().includes(page.id)) {
627
+ pageKey = page.id;
628
+ break;
629
+ }
630
+ }
631
+
632
+ if (!extractedByPage[pageKey]) extractedByPage[pageKey] = {};
633
+
634
+ const res = transformFileContent(file, pageKey, extractedByPage[pageKey], backupDir, projectDir);
635
+ if (res.fileModified) {
636
+ transformedFilesCount++;
637
+ totalTransformedElements += res.elementCount;
638
+ console.log(` \x1b[32m✔ Transformed\x1b[0m ${path.relative(projectDir, file)} (\x1b[33m${res.elementCount}\x1b[0m editable markers added)`);
639
+ }
640
+ }
641
+
642
+ // 3. Harmonize UI Components
643
+ console.log(`\n🧩 Harmonizing UI components in ${detection.uiDir ? path.relative(projectDir, detection.uiDir) : 'src/components/ui'}...`);
644
+ const uiRes = harmonizeUiComponents(projectDir, detection, backupDir);
645
+ if (uiRes.count > 0) {
646
+ console.log(` \x1b[32m✔ Harmonized\x1b[0m ${uiRes.count} component(s) to support visual preview attributes`);
647
+ }
648
+
649
+ // 4. Generate centralized data and synchronized manifest
650
+ console.log(`\n📦 Generating centralized site-data.json and Fivora Spec v2 contract...`);
651
+ const dataRes = generateTemplateData(projectDir, projectName, detectedPages, extractedByPage);
652
+ console.log(` \x1b[32m✔ Generated\x1b[0m ${path.relative(projectDir, dataRes.siteDataPath)}`);
653
+ console.log(` \x1b[32m✔ Generated\x1b[0m ${path.relative(projectDir, dataRes.manifestPath)} (\x1b[36m${dataRes.totalFields}\x1b[0m visual fields mapped)`);
654
+
655
+ return {
656
+ detection,
657
+ backupDir,
658
+ transformedFilesCount,
659
+ totalTransformedElements,
660
+ totalFields: dataRes.totalFields,
661
+ };
662
+ }
663
+
664
+ module.exports = {
665
+ detectProjectFrameworks,
666
+ createBackup,
667
+ instrumentRootLayout,
668
+ transformFileContent,
669
+ harmonizeUiComponents,
670
+ generateTemplateData,
671
+ runUniversalTemplateConversion,
672
+ };