@deneb-ui/cli 2.0.8 → 2.0.10
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 +17 -7
- package/package.json +1 -1
- package/src/tools/template-converter.cjs +672 -0
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.
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
3
|
+
"version": "2.0.10",
|
|
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
|
+
};
|