@deneb-ui/cli 2.0.10 → 2.0.11
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 +16 -4
- package/package.json +3 -1
- package/src/recipes/ecommerce-storefront.json +1163 -0
- package/src/recipes/vanta-shoes-store.json +1138 -0
- package/src/tools/local-template-lab.cjs +1 -1
- package/src/tools/recipe-engine.cjs +158 -0
- package/src/tools/template-converter.cjs +218 -40
- package/src/tools/template-preview-focus-bridge.cjs +1 -1
|
@@ -28,7 +28,7 @@ Preview server stopped during navigation (${e}). Restarting (${b}/${k})...
|
|
|
28
28
|
<iframe id="template-preview" title="Local template preview"></iframe>
|
|
29
29
|
<script>
|
|
30
30
|
(() => {
|
|
31
|
-
const BRIDGE_SOURCE = ${JSON.stringify(
|
|
31
|
+
const BRIDGE_SOURCE = ${JSON.stringify((()=>{try{delete require.cache[require.resolve("./template-preview-focus-bridge.cjs")];const e=require("./template-preview-focus-bridge.cjs");if(typeof e=="string"&&e.trim())return e;}catch{}return ee;})())};
|
|
32
32
|
const preview = document.getElementById('template-preview');
|
|
33
33
|
const errorBox = document.getElementById('bridge-error');
|
|
34
34
|
const PREVIOUS_PREVIEW_PREFIX = ['MARKET', 'PLACE'].join('') + '_PREVIEW_';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DENEB Recipe Engine & Learning System
|
|
3
|
+
*
|
|
4
|
+
* Preserves, identifies, and applies battle-tested storefront transformation recipes.
|
|
5
|
+
* Enables developers and AI agents to save custom fixes from calibrated projects
|
|
6
|
+
* and automatically re-apply them to any new frontend during `npx @deneb-ui/cli init`.
|
|
7
|
+
*
|
|
8
|
+
* Created by Chamika Gayashan & Induranga Kawishwara.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const BUILT_IN_RECIPES_DIR = path.join(__dirname, '..', 'recipes');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load all available recipes (built-in + user-saved)
|
|
18
|
+
*/
|
|
19
|
+
function loadAllRecipes(projectDir) {
|
|
20
|
+
const recipes = [];
|
|
21
|
+
|
|
22
|
+
// 1. Built-in recipes
|
|
23
|
+
if (fs.existsSync(BUILT_IN_RECIPES_DIR)) {
|
|
24
|
+
const files = fs.readdirSync(BUILT_IN_RECIPES_DIR);
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
if (!file.endsWith('.json')) continue;
|
|
27
|
+
try {
|
|
28
|
+
const content = JSON.parse(fs.readFileSync(path.join(BUILT_IN_RECIPES_DIR, file), 'utf8'));
|
|
29
|
+
recipes.push({ ...content, isBuiltIn: true });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Ignore malformed files
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Local workspace / project recipes (.deneb/recipes)
|
|
37
|
+
if (projectDir) {
|
|
38
|
+
const localDir = path.join(projectDir, '.deneb', 'recipes');
|
|
39
|
+
if (fs.existsSync(localDir)) {
|
|
40
|
+
const files = fs.readdirSync(localDir);
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
if (!file.endsWith('.json')) continue;
|
|
43
|
+
try {
|
|
44
|
+
const content = JSON.parse(fs.readFileSync(path.join(localDir, file), 'utf8'));
|
|
45
|
+
recipes.push({ ...content, isBuiltIn: false, source: 'local' });
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return recipes;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Match the best recipe for an incoming project based on signatures
|
|
56
|
+
*/
|
|
57
|
+
function matchRecipeForProject(projectDir, pkg = {}, sourceFiles = []) {
|
|
58
|
+
const allRecipes = loadAllRecipes(projectDir);
|
|
59
|
+
if (allRecipes.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
const pkgStr = JSON.stringify(pkg).toLowerCase();
|
|
62
|
+
const fileNamesStr = sourceFiles.map((f) => path.basename(f).toLowerCase()).join(' ');
|
|
63
|
+
|
|
64
|
+
let bestMatch = null;
|
|
65
|
+
let highestScore = 0;
|
|
66
|
+
|
|
67
|
+
for (const recipe of allRecipes) {
|
|
68
|
+
let score = 0;
|
|
69
|
+
const signatures = recipe.signatures || {};
|
|
70
|
+
const keywords = signatures.keywords || [];
|
|
71
|
+
const roles = signatures.componentRoles || [];
|
|
72
|
+
|
|
73
|
+
for (const kw of keywords) {
|
|
74
|
+
if (pkgStr.includes(kw)) score += 3;
|
|
75
|
+
if (fileNamesStr.includes(kw)) score += 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const role of roles) {
|
|
79
|
+
if (fileNamesStr.includes(role.toLowerCase())) score += 5;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (score > highestScore) {
|
|
83
|
+
highestScore = score;
|
|
84
|
+
bestMatch = recipe;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If score is decent, return recipe; otherwise default to ecommerce-storefront if available
|
|
89
|
+
if (highestScore >= 6) {
|
|
90
|
+
return bestMatch;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const defaultRecipe = allRecipes.find((r) => r.name === 'ecommerce-storefront');
|
|
94
|
+
return defaultRecipe || allRecipes[0] || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Learn & Save Recipe from an existing calibrated project
|
|
99
|
+
*/
|
|
100
|
+
function saveRecipeFromProject(projectDir, recipeName = 'custom-storefront', options = {}) {
|
|
101
|
+
const manifestPath = path.join(projectDir, 'fivora-template.json');
|
|
102
|
+
const siteDataPath = path.join(projectDir, 'src', 'data', 'site-data.json');
|
|
103
|
+
|
|
104
|
+
if (!fs.existsSync(manifestPath)) {
|
|
105
|
+
throw new Error(`fivora-template.json not found in ${projectDir}. Make sure the project is calibrated first.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
109
|
+
let siteData = {};
|
|
110
|
+
if (fs.existsSync(siteDataPath)) {
|
|
111
|
+
siteData = JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const recipeId = recipeName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
115
|
+
const recipe = {
|
|
116
|
+
name: recipeId,
|
|
117
|
+
label: options.label || manifest.name || recipeName,
|
|
118
|
+
description: options.description || `Learned and saved recipe extracted from ${path.basename(projectDir)}.`,
|
|
119
|
+
savedAt: new Date().toISOString(),
|
|
120
|
+
signatures: {
|
|
121
|
+
keywords: [recipeId, 'store', 'shop', 'product'],
|
|
122
|
+
componentRoles: ['Navbar', 'Hero', 'ProductGrid', 'ProductCard', 'Footer'],
|
|
123
|
+
},
|
|
124
|
+
gridRules: {
|
|
125
|
+
cardComponentNames: ['ProductCard', 'Card', 'ItemCard', 'ShoeCard'],
|
|
126
|
+
cardFields: [
|
|
127
|
+
{ suffix: 'Image', type: 'image', label: 'Image' },
|
|
128
|
+
{ suffix: 'Badge', type: 'text', label: 'Badge' },
|
|
129
|
+
{ suffix: 'Category', type: 'text', label: 'Category' },
|
|
130
|
+
{ suffix: 'Name', type: 'text', label: 'Title' },
|
|
131
|
+
{ suffix: 'Price', type: 'text', label: 'Price' },
|
|
132
|
+
],
|
|
133
|
+
autoIndex: true,
|
|
134
|
+
defaultCardPrefix: 'product',
|
|
135
|
+
},
|
|
136
|
+
sections: manifest.editorSchema?.sections || [],
|
|
137
|
+
defaults: siteData.content || {},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// 1. Save to global CLI recipes directory
|
|
141
|
+
fs.mkdirSync(BUILT_IN_RECIPES_DIR, { recursive: true });
|
|
142
|
+
const globalDest = path.join(BUILT_IN_RECIPES_DIR, `${recipeId}.json`);
|
|
143
|
+
fs.writeFileSync(globalDest, JSON.stringify(recipe, null, 2) + '\n', 'utf8');
|
|
144
|
+
|
|
145
|
+
// 2. Also save to project-local .deneb/recipes directory for portability
|
|
146
|
+
const localDestDir = path.join(projectDir, '.deneb', 'recipes');
|
|
147
|
+
fs.mkdirSync(localDestDir, { recursive: true });
|
|
148
|
+
const localDest = path.join(localDestDir, `${recipeId}.json`);
|
|
149
|
+
fs.writeFileSync(localDest, JSON.stringify(recipe, null, 2) + '\n', 'utf8');
|
|
150
|
+
|
|
151
|
+
return { recipe, globalDest, localDest };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
loadAllRecipes,
|
|
156
|
+
matchRecipeForProject,
|
|
157
|
+
saveRecipeFromProject,
|
|
158
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DENEB Universal Template Converter Engine
|
|
2
|
+
* DENEB Universal Template Converter Engine & Recipe System
|
|
3
3
|
*
|
|
4
4
|
* Automatically converts existing Next.js projects (built with shadcn/ui,
|
|
5
5
|
* HeroUI, Tailwind CSS, or custom React components) into fully editable
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
+
const { matchRecipeForProject, saveRecipeFromProject } = require('./recipe-engine.cjs');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 1. Detect CSS and Component Frameworks
|
|
@@ -218,6 +219,7 @@ function findSourceFiles(dir, fileList = []) {
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
}
|
|
222
|
+
|
|
221
223
|
return fileList;
|
|
222
224
|
}
|
|
223
225
|
|
|
@@ -243,9 +245,9 @@ function toFieldKey(text, prefix = 'text', index = 1) {
|
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
/**
|
|
246
|
-
* 5. Intelligent JSX Content Extractor and Marker Transformer
|
|
248
|
+
* 5. Intelligent JSX Content Extractor and Marker Transformer with Heuristic Recipes
|
|
247
249
|
*/
|
|
248
|
-
function transformFileContent(filePath, pageKey, extractedData, backupDir, projectDir) {
|
|
250
|
+
function transformFileContent(filePath, pageKey, extractedData, backupDir, projectDir, activeRecipe = null) {
|
|
249
251
|
let code = fs.readFileSync(filePath, 'utf8');
|
|
250
252
|
|
|
251
253
|
let fileModified = false;
|
|
@@ -268,7 +270,7 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
268
270
|
}
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
// 2.
|
|
273
|
+
// 2. Headings: <h1> to <h6>
|
|
272
274
|
const headingRegex = /<(h[1-6])(\s+[^>]*)?>([^<>{}]+)<\/\1>/g;
|
|
273
275
|
code = code.replace(headingRegex, (match, tag, attrs = '', text) => {
|
|
274
276
|
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
@@ -282,10 +284,10 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
282
284
|
elementCount++;
|
|
283
285
|
fileModified = true;
|
|
284
286
|
|
|
285
|
-
return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
287
|
+
return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</${tag}>`;
|
|
286
288
|
});
|
|
287
289
|
|
|
288
|
-
// 3.
|
|
290
|
+
// 3. Paragraphs: <p>
|
|
289
291
|
const pRegex = /<p(\s+[^>]*)?>([^<>{}]+)<\/p>/g;
|
|
290
292
|
code = code.replace(pRegex, (match, attrs = '', text) => {
|
|
291
293
|
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
@@ -299,10 +301,10 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
299
301
|
elementCount++;
|
|
300
302
|
fileModified = true;
|
|
301
303
|
|
|
302
|
-
return `<p data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
304
|
+
return `<p data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</p>`;
|
|
303
305
|
});
|
|
304
306
|
|
|
305
|
-
// 4.
|
|
307
|
+
// 4. Card Titles & Descriptions (shadcn/ui & modern patterns)
|
|
306
308
|
const cardTitleRegex = /<CardTitle(\s+[^>]*)?>([^<>{}]+)<\/CardTitle>/g;
|
|
307
309
|
code = code.replace(cardTitleRegex, (match, attrs = '', text) => {
|
|
308
310
|
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
@@ -316,7 +318,7 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
316
318
|
elementCount++;
|
|
317
319
|
fileModified = true;
|
|
318
320
|
|
|
319
|
-
return `<CardTitle data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
321
|
+
return `<CardTitle data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</CardTitle>`;
|
|
320
322
|
});
|
|
321
323
|
|
|
322
324
|
const cardDescRegex = /<CardDescription(\s+[^>]*)?>([^<>{}]+)<\/CardDescription>/g;
|
|
@@ -332,10 +334,10 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
332
334
|
elementCount++;
|
|
333
335
|
fileModified = true;
|
|
334
336
|
|
|
335
|
-
return `<CardDescription data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
337
|
+
return `<CardDescription data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</CardDescription>`;
|
|
336
338
|
});
|
|
337
339
|
|
|
338
|
-
// 5.
|
|
340
|
+
// 5. Buttons & CTAs: <Button> and <button>
|
|
339
341
|
const btnRegex = /<(Button|button)(\s+[^>]*)?>([^<>{}]+)<\/\1>/g;
|
|
340
342
|
code = code.replace(btnRegex, (match, tag, attrs = '', text) => {
|
|
341
343
|
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
@@ -349,10 +351,10 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
349
351
|
elementCount++;
|
|
350
352
|
fileModified = true;
|
|
351
353
|
|
|
352
|
-
return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
354
|
+
return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</${tag}>`;
|
|
353
355
|
});
|
|
354
356
|
|
|
355
|
-
// 6.
|
|
357
|
+
// 6. Inputs & Search Bars: placeholder attribute
|
|
356
358
|
const inputRegex = /<(input|Input|textarea|Textarea)(\s+[^>]*?)placeholder="([^"]+)"([^>]*?)\/?>/g;
|
|
357
359
|
code = code.replace(inputRegex, (match, tag, beforeAttrs = '', placeholder, afterAttrs = '') => {
|
|
358
360
|
const trimmed = placeholder.trim();
|
|
@@ -366,10 +368,10 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
366
368
|
elementCount++;
|
|
367
369
|
fileModified = true;
|
|
368
370
|
|
|
369
|
-
return `<${tag}${beforeAttrs}data-preview-field-path="${pageKey}.${fieldKey}" placeholder={siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
371
|
+
return `<${tag}${beforeAttrs}data-preview-field-path="${pageKey}.${fieldKey}" placeholder={siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}${afterAttrs}/>`;
|
|
370
372
|
});
|
|
371
373
|
|
|
372
|
-
// 7. Extract
|
|
374
|
+
// 7. Extract Images: <img src="..." alt="..." /> or <Image ... />
|
|
373
375
|
const imgRegex = /<(img|Image)(\s+[^>]*?)src="([^"]+)"([^>]*?)alt="([^"]+)"([^>]*?)\/?>/g;
|
|
374
376
|
code = code.replace(imgRegex, (match, tag, preSrc = '', src, mid = '', alt, post = '') => {
|
|
375
377
|
if (preSrc.includes('data-preview-field-path') || mid.includes('data-preview-field-path') || post.includes('data-preview-field-path')) {
|
|
@@ -383,7 +385,128 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
383
385
|
elementCount++;
|
|
384
386
|
fileModified = true;
|
|
385
387
|
|
|
386
|
-
return `<${tag}${preSrc}data-preview-field-path="${pageKey}.${fieldKey}" src={siteData?.content?.${pageKey}?.${fieldKey} ||
|
|
388
|
+
return `<${tag}${preSrc}data-preview-field-path="${pageKey}.${fieldKey}" src={siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(src)}}${mid}alt="${alt}"${post}/>`;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// 8. Badges, Labels, Tags: <span ...>
|
|
392
|
+
const spanRegex = /<span(\s+[^>]*)?>([^<>{}]+)<\/span>/g;
|
|
393
|
+
code = code.replace(spanRegex, (match, attrs = '', text) => {
|
|
394
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
395
|
+
if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path') || attrs.includes('data-preview-page-key')) {
|
|
396
|
+
return match;
|
|
397
|
+
}
|
|
398
|
+
if (/^[0-9]+$/.test(trimmed) && trimmed.length < 2) return match;
|
|
399
|
+
const rawKey = toFieldKey(trimmed, 'label', elementCount + 1);
|
|
400
|
+
const fieldKey = getUniqueKey(rawKey);
|
|
401
|
+
|
|
402
|
+
extractedData[fieldKey] = trimmed;
|
|
403
|
+
elementCount++;
|
|
404
|
+
fileModified = true;
|
|
405
|
+
|
|
406
|
+
return `<span data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</span>`;
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// 9. Anchor Links: <a> (sensitive attribute handler)
|
|
410
|
+
const linkRegex = /<a(\s+[^>]*)?>([^<>{}]+)<\/a>/g;
|
|
411
|
+
code = code.replace(linkRegex, (match, attrs = '', text) => {
|
|
412
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
413
|
+
if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
|
|
414
|
+
return match;
|
|
415
|
+
}
|
|
416
|
+
const rawKey = toFieldKey(trimmed, 'linkText', elementCount + 1);
|
|
417
|
+
const fieldKey = getUniqueKey(rawKey);
|
|
418
|
+
|
|
419
|
+
extractedData[fieldKey] = trimmed;
|
|
420
|
+
elementCount++;
|
|
421
|
+
fileModified = true;
|
|
422
|
+
|
|
423
|
+
return `<a data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</a>`;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// 10. List items: <li>
|
|
427
|
+
const liRegex = /<li(\s+[^>]*)?>([^<>{}]+)<\/li>/g;
|
|
428
|
+
code = code.replace(liRegex, (match, attrs = '', text) => {
|
|
429
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
430
|
+
if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
|
|
431
|
+
return match;
|
|
432
|
+
}
|
|
433
|
+
const rawKey = toFieldKey(trimmed, 'item', elementCount + 1);
|
|
434
|
+
const fieldKey = getUniqueKey(rawKey);
|
|
435
|
+
|
|
436
|
+
extractedData[fieldKey] = trimmed;
|
|
437
|
+
elementCount++;
|
|
438
|
+
fileModified = true;
|
|
439
|
+
|
|
440
|
+
return `<li data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</li>`;
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// 11. UI Badges: <Badge> or <badge>
|
|
444
|
+
const badgeRegex = /<(Badge|badge)(\s+[^>]*)?>([^<>{}]+)<\/\1>/g;
|
|
445
|
+
code = code.replace(badgeRegex, (match, tag, attrs = '', text) => {
|
|
446
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
447
|
+
if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
|
|
448
|
+
return match;
|
|
449
|
+
}
|
|
450
|
+
const rawKey = toFieldKey(trimmed, 'badge', elementCount + 1);
|
|
451
|
+
const fieldKey = getUniqueKey(rawKey);
|
|
452
|
+
|
|
453
|
+
extractedData[fieldKey] = trimmed;
|
|
454
|
+
elementCount++;
|
|
455
|
+
fileModified = true;
|
|
456
|
+
|
|
457
|
+
return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</${tag}>`;
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// 12. UI Typography: Heading, Title, Subtitle, Description, Typography
|
|
461
|
+
const typoRegex = /<(Heading|Title|Subtitle|Description|Typography)(\s+[^>]*)?>([^<>{}]+)<\/\1>/g;
|
|
462
|
+
code = code.replace(typoRegex, (match, tag, attrs = '', text) => {
|
|
463
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
464
|
+
if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
|
|
465
|
+
return match;
|
|
466
|
+
}
|
|
467
|
+
const rawKey = toFieldKey(trimmed, tag.toLowerCase(), elementCount + 1);
|
|
468
|
+
const fieldKey = getUniqueKey(rawKey);
|
|
469
|
+
|
|
470
|
+
extractedData[fieldKey] = trimmed;
|
|
471
|
+
elementCount++;
|
|
472
|
+
fileModified = true;
|
|
473
|
+
|
|
474
|
+
return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</${tag}>`;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// 13. Styled text in <div> (e.g. shadow text, banners, overlays, stats, prices)
|
|
478
|
+
const divTextRegex = /<div(\s+[^>]*?class(?:Name)?="[^"]*(?:bg|shadow|watermark|banner|overlay|hero|title|heading|text|label|sub|desc|caption|badge|price|quote|stat|tag|brand|lead)[^"]*"[^>]*)>([^<>{}]+)<\/div>/gi;
|
|
479
|
+
code = code.replace(divTextRegex, (match, attrs = '', text) => {
|
|
480
|
+
const trimmed = text.trim().replace(/\s+/g, ' ');
|
|
481
|
+
if (!trimmed || trimmed.length < 2 || attrs.includes('data-preview-field-path')) {
|
|
482
|
+
return match;
|
|
483
|
+
}
|
|
484
|
+
const rawKey = toFieldKey(trimmed, 'divText', elementCount + 1);
|
|
485
|
+
const fieldKey = getUniqueKey(rawKey);
|
|
486
|
+
|
|
487
|
+
extractedData[fieldKey] = trimmed;
|
|
488
|
+
elementCount++;
|
|
489
|
+
fileModified = true;
|
|
490
|
+
|
|
491
|
+
return `<div data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</div>`;
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// 14. Sensitive Element Guardian: Auto-annotate non-bound <a> and <img> tags with data-preview-static
|
|
495
|
+
// Ensures 100% compliance with Fivora strict mode so unannotated links/images don't fail certification
|
|
496
|
+
code = code.replace(/<a(\s+[^>]*?href="[^"]*"[^>]*?)>/gi, (match, attrs) => {
|
|
497
|
+
if (attrs.includes('data-preview-field-path') || attrs.includes('data-preview-static')) {
|
|
498
|
+
return match;
|
|
499
|
+
}
|
|
500
|
+
fileModified = true;
|
|
501
|
+
return `<a${attrs} data-preview-static="navigation-link">`;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
code = code.replace(/<img(\s+[^>]*?src="[^"]*"[^>]*?)>/gi, (match, attrs) => {
|
|
505
|
+
if (attrs.includes('data-preview-field-path') || attrs.includes('data-preview-static')) {
|
|
506
|
+
return match;
|
|
507
|
+
}
|
|
508
|
+
fileModified = true;
|
|
509
|
+
return `<img${attrs} data-preview-static="decorative-image">`;
|
|
387
510
|
});
|
|
388
511
|
|
|
389
512
|
// If file was modified, ensure useSiteData and client directives are added
|
|
@@ -391,7 +514,6 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
391
514
|
backupFile(filePath, projectDir, backupDir);
|
|
392
515
|
|
|
393
516
|
// 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
517
|
if (code.includes('export const metadata') || code.includes('export let metadata')) {
|
|
396
518
|
code = code.replace(/export\s+(const|let)\s+metadata/g, '// Metadata preserved for static export\n$1 metadata');
|
|
397
519
|
}
|
|
@@ -412,22 +534,18 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
412
534
|
// Add const { siteData } = useSiteData(); inside the primary component function
|
|
413
535
|
if (!code.includes('useSiteData()')) {
|
|
414
536
|
let injected = false;
|
|
415
|
-
// Match export default function Name(...) {
|
|
416
537
|
if (/(export\s+default\s+function\s*[A-Za-z0-9_]*\s*\([^)]*\)\s*\{)/.test(code)) {
|
|
417
538
|
code = code.replace(/(export\s+default\s+function\s*[A-Za-z0-9_]*\s*\([^)]*\)\s*\{)/, `$1\n const { siteData } = useSiteData();`);
|
|
418
539
|
injected = true;
|
|
419
540
|
}
|
|
420
|
-
// Match export default (...) => {
|
|
421
541
|
if (!injected && /(export\s+default\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/.test(code)) {
|
|
422
542
|
code = code.replace(/(export\s+default\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/, `$1\n const { siteData } = useSiteData();`);
|
|
423
543
|
injected = true;
|
|
424
544
|
}
|
|
425
|
-
// Match const ComponentName = (...) => {
|
|
426
545
|
if (!injected && /(const\s+[A-Za-z0-9_]+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/.test(code)) {
|
|
427
546
|
code = code.replace(/(const\s+[A-Za-z0-9_]+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{)/, `$1\n const { siteData } = useSiteData();`);
|
|
428
547
|
injected = true;
|
|
429
548
|
}
|
|
430
|
-
// Match function ComponentName(...) {
|
|
431
549
|
if (!injected && /(function\s+[A-Za-z0-9_]+\s*\([^)]*\)\s*\{)/.test(code)) {
|
|
432
550
|
code = code.replace(/(function\s+[A-Za-z0-9_]+\s*\([^)]*\)\s*\{)/, `$1\n const { siteData } = useSiteData();`);
|
|
433
551
|
injected = true;
|
|
@@ -465,20 +583,27 @@ function harmonizeUiComponents(projectDir, detection, backupDir) {
|
|
|
465
583
|
}
|
|
466
584
|
|
|
467
585
|
/**
|
|
468
|
-
* 7. Generate Comprehensive site-data.json and fivora-template.json
|
|
586
|
+
* 7. Generate Comprehensive site-data.json and fivora-template.json with Recipe Defaults
|
|
469
587
|
*/
|
|
470
|
-
function generateTemplateData(projectDir, projectName, detectedPages, extractedByPage) {
|
|
588
|
+
function generateTemplateData(projectDir, projectName, detectedPages, extractedByPage, activeRecipe = null) {
|
|
471
589
|
const manifestPath = path.join(projectDir, 'fivora-template.json');
|
|
472
590
|
const siteDataPath = path.join(projectDir, 'src', 'data', 'site-data.json');
|
|
473
591
|
fs.mkdirSync(path.dirname(siteDataPath), { recursive: true });
|
|
474
592
|
|
|
593
|
+
const navLabels = {};
|
|
594
|
+
for (const p of detectedPages) {
|
|
595
|
+
navLabels[p.id] = p.label || p.id;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Base content
|
|
475
599
|
const content = {
|
|
476
600
|
common: {
|
|
477
601
|
websiteTitle: projectName,
|
|
478
602
|
shortDescription: `A high-converting storefront built for the Fivora platform.`,
|
|
479
603
|
logoUrl: '/fivora-logo.png',
|
|
480
|
-
headerCtaLabel: '
|
|
481
|
-
copyright:
|
|
604
|
+
headerCtaLabel: 'Shop Now',
|
|
605
|
+
copyright: `© ${new Date().getFullYear()} ${projectName}. All rights reserved.`,
|
|
606
|
+
navLabels: navLabels,
|
|
482
607
|
business: {
|
|
483
608
|
phone: '+1 (555) 482-9012',
|
|
484
609
|
whatsapp: '15554829012',
|
|
@@ -498,39 +623,84 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
|
|
|
498
623
|
{ key: 'shortDescription', type: 'textarea', label: 'Short Description' },
|
|
499
624
|
{ key: 'logoUrl', type: 'image', label: 'Website Logo' },
|
|
500
625
|
{ key: 'headerCtaLabel', type: 'text', label: 'Header CTA Button' },
|
|
626
|
+
{
|
|
627
|
+
key: 'navLabels',
|
|
628
|
+
type: 'object',
|
|
629
|
+
label: 'Navigation Labels',
|
|
630
|
+
fields: detectedPages.map((p) => ({
|
|
631
|
+
key: p.id,
|
|
632
|
+
type: 'text',
|
|
633
|
+
label: `${p.label || p.id} Link`,
|
|
634
|
+
})),
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
key: 'business',
|
|
638
|
+
type: 'object',
|
|
639
|
+
label: 'Business Information',
|
|
640
|
+
fields: [
|
|
641
|
+
{ key: 'phone', type: 'tel', label: 'Phone Number' },
|
|
642
|
+
{ key: 'whatsapp', type: 'text', label: 'WhatsApp Number' },
|
|
643
|
+
{ key: 'email', type: 'email', label: 'Contact Email' },
|
|
644
|
+
],
|
|
645
|
+
},
|
|
646
|
+
{ key: 'copyright', type: 'text', label: 'Copyright' },
|
|
501
647
|
],
|
|
502
648
|
},
|
|
503
649
|
];
|
|
504
650
|
|
|
651
|
+
// If active recipe has defined sections, seed them
|
|
652
|
+
if (activeRecipe && Array.isArray(activeRecipe.sections)) {
|
|
653
|
+
for (const sec of activeRecipe.sections) {
|
|
654
|
+
if (sec.id === 'common') continue;
|
|
655
|
+
editorSections.push(sec);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Seed recipe defaults if available
|
|
660
|
+
if (activeRecipe && activeRecipe.defaults) {
|
|
661
|
+
for (const [secKey, secVal] of Object.entries(activeRecipe.defaults)) {
|
|
662
|
+
if (!content[secKey]) content[secKey] = {};
|
|
663
|
+
Object.assign(content[secKey], secVal);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Merge dynamically extracted fields
|
|
505
668
|
for (const page of detectedPages) {
|
|
506
669
|
const pageKey = page.id;
|
|
507
670
|
const pageFields = extractedByPage[pageKey] || {};
|
|
508
671
|
|
|
509
|
-
content[pageKey] = {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
672
|
+
if (!content[pageKey]) content[pageKey] = {};
|
|
673
|
+
Object.assign(content[pageKey], pageFields);
|
|
674
|
+
|
|
675
|
+
// Check if section already exists in editorSections
|
|
676
|
+
let existingSection = editorSections.find((s) => s.id === pageKey);
|
|
677
|
+
const newFieldDefs = [];
|
|
513
678
|
|
|
514
|
-
const
|
|
679
|
+
for (const [key, val] of Object.entries(pageFields)) {
|
|
680
|
+
if (existingSection && existingSection.fields.some((f) => f.key === key)) {
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
515
683
|
const isImg = key.toLowerCase().includes('image') || (typeof val === 'string' && /\.(jpg|png|webp|svg)$/i.test(val));
|
|
516
684
|
const isLong = typeof val === 'string' && val.length > 60;
|
|
517
|
-
|
|
685
|
+
newFieldDefs.push({
|
|
518
686
|
key: key,
|
|
519
687
|
type: isImg ? 'image' : isLong ? 'textarea' : 'text',
|
|
520
688
|
label: key
|
|
521
689
|
.replace(/([A-Z])/g, ' $1')
|
|
522
690
|
.replace(/[-_]/g, ' ')
|
|
523
691
|
.replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
524
|
-
};
|
|
525
|
-
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
526
694
|
|
|
527
|
-
if (
|
|
695
|
+
if (existingSection) {
|
|
696
|
+
existingSection.fields.push(...newFieldDefs);
|
|
697
|
+
} else if (newFieldDefs.length > 0) {
|
|
528
698
|
editorSections.push({
|
|
529
699
|
id: pageKey,
|
|
530
700
|
path: pageKey,
|
|
531
701
|
type: 'object',
|
|
532
702
|
label: `${page.label || pageKey} Content`,
|
|
533
|
-
fields:
|
|
703
|
+
fields: newFieldDefs,
|
|
534
704
|
});
|
|
535
705
|
}
|
|
536
706
|
}
|
|
@@ -566,7 +736,7 @@ function generateTemplateData(projectDir, projectName, detectedPages, extractedB
|
|
|
566
736
|
visualEditing: {
|
|
567
737
|
contractVersion: 1,
|
|
568
738
|
mode: 'strict',
|
|
569
|
-
controlOnlyPaths: [],
|
|
739
|
+
controlOnlyPaths: ['common.brandUrl'],
|
|
570
740
|
},
|
|
571
741
|
siteDataFile: 'src/data/site-data.json',
|
|
572
742
|
outputDirectory: 'out',
|
|
@@ -596,6 +766,14 @@ function runUniversalTemplateConversion(projectDir, projectName, detectedPages)
|
|
|
596
766
|
console.log(` \x1b[36m✔ Detected:\x1b[0m ${framework}`);
|
|
597
767
|
}
|
|
598
768
|
|
|
769
|
+
const allSourceFiles = findSourceFiles(projectDir);
|
|
770
|
+
|
|
771
|
+
// Match optimal recipe (or custom saved fixes)
|
|
772
|
+
const matchedRecipe = matchRecipeForProject(projectDir, detection.pkg, allSourceFiles);
|
|
773
|
+
if (matchedRecipe) {
|
|
774
|
+
console.log(` \x1b[35m🎯 Matched Recipe:\x1b[0m ${matchedRecipe.label} (${matchedRecipe.name})`);
|
|
775
|
+
}
|
|
776
|
+
|
|
599
777
|
const backupDir = createBackup(projectDir);
|
|
600
778
|
console.log(`\n🛡️ Created safe backup at \x1b[90m${path.basename(backupDir)}\x1b[0m`);
|
|
601
779
|
|
|
@@ -610,7 +788,6 @@ function runUniversalTemplateConversion(projectDir, projectName, detectedPages)
|
|
|
610
788
|
|
|
611
789
|
// 2. Scan & Transform Pages and Components
|
|
612
790
|
console.log(`\n⚡ Scanning & instrumenting pages with visual editing markers...`);
|
|
613
|
-
const allSourceFiles = findSourceFiles(projectDir);
|
|
614
791
|
const extractedByPage = {};
|
|
615
792
|
let totalTransformedElements = 0;
|
|
616
793
|
let transformedFilesCount = 0;
|
|
@@ -620,7 +797,6 @@ function runUniversalTemplateConversion(projectDir, projectName, detectedPages)
|
|
|
620
797
|
}
|
|
621
798
|
|
|
622
799
|
for (const file of allSourceFiles) {
|
|
623
|
-
// Determine page association
|
|
624
800
|
let pageKey = 'home';
|
|
625
801
|
for (const page of detectedPages) {
|
|
626
802
|
if (page.id !== 'home' && file.toLowerCase().includes(page.id)) {
|
|
@@ -631,7 +807,7 @@ function runUniversalTemplateConversion(projectDir, projectName, detectedPages)
|
|
|
631
807
|
|
|
632
808
|
if (!extractedByPage[pageKey]) extractedByPage[pageKey] = {};
|
|
633
809
|
|
|
634
|
-
const res = transformFileContent(file, pageKey, extractedByPage[pageKey], backupDir, projectDir);
|
|
810
|
+
const res = transformFileContent(file, pageKey, extractedByPage[pageKey], backupDir, projectDir, matchedRecipe);
|
|
635
811
|
if (res.fileModified) {
|
|
636
812
|
transformedFilesCount++;
|
|
637
813
|
totalTransformedElements += res.elementCount;
|
|
@@ -648,13 +824,14 @@ function runUniversalTemplateConversion(projectDir, projectName, detectedPages)
|
|
|
648
824
|
|
|
649
825
|
// 4. Generate centralized data and synchronized manifest
|
|
650
826
|
console.log(`\n📦 Generating centralized site-data.json and Fivora Spec v2 contract...`);
|
|
651
|
-
const dataRes = generateTemplateData(projectDir, projectName, detectedPages, extractedByPage);
|
|
827
|
+
const dataRes = generateTemplateData(projectDir, projectName, detectedPages, extractedByPage, matchedRecipe);
|
|
652
828
|
console.log(` \x1b[32m✔ Generated\x1b[0m ${path.relative(projectDir, dataRes.siteDataPath)}`);
|
|
653
829
|
console.log(` \x1b[32m✔ Generated\x1b[0m ${path.relative(projectDir, dataRes.manifestPath)} (\x1b[36m${dataRes.totalFields}\x1b[0m visual fields mapped)`);
|
|
654
830
|
|
|
655
831
|
return {
|
|
656
832
|
detection,
|
|
657
833
|
backupDir,
|
|
834
|
+
matchedRecipe,
|
|
658
835
|
transformedFilesCount,
|
|
659
836
|
totalTransformedElements,
|
|
660
837
|
totalFields: dataRes.totalFields,
|
|
@@ -669,4 +846,5 @@ module.exports = {
|
|
|
669
846
|
harmonizeUiComponents,
|
|
670
847
|
generateTemplateData,
|
|
671
848
|
runUniversalTemplateConversion,
|
|
849
|
+
saveRecipeFromProject,
|
|
672
850
|
};
|