@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.
@@ -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(D)};
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. Extract Headings: <h1> to <h6> (multiline-safe)
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} || "${trimmed}"}</${tag}>`;
287
+ return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</${tag}>`;
286
288
  });
287
289
 
288
- // 3. Extract Paragraphs: <p> (multiline-safe)
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} || "${trimmed}"}</p>`;
304
+ return `<p data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</p>`;
303
305
  });
304
306
 
305
- // 4. Extract CardTitle & CardDescription (shadcn/ui & modern patterns, multiline-safe)
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} || "${trimmed}"}</CardTitle>`;
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} || "${trimmed}"}</CardDescription>`;
337
+ return `<CardDescription data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</CardDescription>`;
336
338
  });
337
339
 
338
- // 5. Extract Buttons & CTAs: <Button> and <button> (multiline-safe)
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} || "${trimmed}"}</${tag}>`;
354
+ return `<${tag} data-preview-field-path="${pageKey}.${fieldKey}"${attrs}>{siteData?.content?.${pageKey}?.${fieldKey} || ${JSON.stringify(trimmed)}}</${tag}>`;
353
355
  });
354
356
 
355
- // 6. Extract Inputs & Search Bars: placeholder attribute
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} || "${trimmed}"}${afterAttrs}/>`;
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 Image alt & src: <img src="..." alt="..." /> or <Image ... />
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} || "${src}"}${mid}alt="${alt}"${post}/>`;
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: 'Contact Us',
481
- copyright: `${projectName}. All rights reserved.`,
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
- ...(content[pageKey] || {}),
511
- ...pageFields,
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 sectionFields = Object.entries(pageFields).map(([key, val]) => {
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
- return {
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 (sectionFields.length > 0) {
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: sectionFields,
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
  };