@deneb-ui/cli 2.0.54 → 2.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -720,6 +720,13 @@ async function initProject(targetInput, options = {}) {
720
720
  addedCount++;
721
721
  }
722
722
  }
723
+ // Ensure Node 20 LTS platform engine compatibility
724
+ pkg.overrides = pkg.overrides || {};
725
+ if (!pkg.overrides['content-type']) {
726
+ pkg.overrides['content-type'] = '2.1.0';
727
+ pkg.overrides['@octokit/request'] = { 'content-type': '2.1.0' };
728
+ }
729
+
723
730
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
724
731
  if (addedCount > 0) {
725
732
  console.log(`\x1b[32m✔ Configured\x1b[0m DENEB scripts in package.json (lab, validate, zip, validate-and-zip, package:template, update:deneb)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
4
4
  "description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
5
5
  "bin": {
6
6
  "deneb": "bin/index.js",
@@ -49,7 +49,7 @@
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
51
  "@babel/parser": "^7.28.0",
52
- "@deneb-ui/core": "^2.0.54",
52
+ "@deneb-ui/core": "^2.0.56",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -58,5 +58,8 @@
58
58
  "devDependencies": {
59
59
  "esbuild": "^0.28.2",
60
60
  "typescript": "^5.9.3"
61
+ },
62
+ "overrides": {
63
+ "content-type": "2.1.0"
61
64
  }
62
65
  }
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const PLATFORM_CONTRACT = require('../platform/platform-contract.json');
4
+
5
+
3
6
  /**
4
7
  * Faithful port of the Fivora strict visual-editing contract rules that the
5
8
  * platform ingest pipeline applies (backend/src/common/template-visual-edit-contract.ts).
@@ -64,7 +67,14 @@ function pathsOverlap(left, right) {
64
67
  return wildcardPath(left) === wildcardPath(right);
65
68
  }
66
69
 
70
+ function isPlatformControlled(path) {
71
+ return (PLATFORM_CONTRACT.platformControlledPaths || []).some((platformPath) =>
72
+ platformPath.includes('[*]') ? wildcardPath(path) === platformPath : path === platformPath
73
+ );
74
+ }
75
+
67
76
  function isControlOnly(path, controlOnlyPaths) {
77
+ if (isPlatformControlled(path)) return true;
68
78
  return (controlOnlyPaths || []).some((controlPath) =>
69
79
  controlPath.includes('[*]') ? wildcardPath(path) === controlPath : path === controlPath
70
80
  );
@@ -5,6 +5,8 @@ const { readJsonSafe, writeJson, deepMerge, isPlainObject } = require('./fs-util
5
5
  const { ARC_VERSION, SCHEMA_VERSION } = require('./version.cjs');
6
6
  const { collectFontIdsFromSiteData, applyFontTheme } = require('./font-plan.cjs');
7
7
  const { humanLabel, classifyFieldType } = require('./field-paths.cjs');
8
+ const PLATFORM_CONTRACT = require('../platform/platform-contract.json');
9
+
8
10
  const {
9
11
  enumerateContentPaths,
10
12
  canonicalizeMarkerPath,
@@ -294,9 +296,16 @@ function baseContent(projectName, routes) {
294
296
  * data-preview-field-path marker or be declared control-only. Control-only is
295
297
  * reserved for ids, internal flags, and the unrendered merchant baseline — not
296
298
  * as a dump for fields ARC planned but failed to bind.
299
+ *
300
+ * PLATFORM_CONTROLLED_PATHS: paths the Fivora AI/platform writes that templates
301
+ * must never render as inline editable HTML. Loaded from platform-contract.json
302
+ * so all templates benefit automatically without any template-side declaration.
297
303
  */
298
304
  const BASELINE_CONTROL_ONLY = /^(common\.(websiteTitle|shortDescription|logoUrl|headerCtaLabel|copyright|navLabels(\.[^.]+)?|business(\.[^.]+)*))$/;
299
305
  const SYSTEM_FIELD = /(^|\.)(id|key|slug|internalId|sku|_id)$/i;
306
+ const PLATFORM_CONTROLLED_PATHS = new Set(
307
+ (PLATFORM_CONTRACT.platformControlledPaths || []).map(wildcardPath)
308
+ );
300
309
 
301
310
  function slimListItems(items, itemFields) {
302
311
  const keys = (itemFields || []).map((field) => field.key).filter(Boolean);
@@ -314,7 +323,56 @@ function slimListItems(items, itemFields) {
314
323
  }
315
324
 
316
325
  function isAllowedControlOnly(path) {
317
- return BASELINE_CONTROL_ONLY.test(path) || SYSTEM_FIELD.test(path);
326
+ return (
327
+ BASELINE_CONTROL_ONLY.test(path) ||
328
+ SYSTEM_FIELD.test(path) ||
329
+ PLATFORM_CONTROLLED_PATHS.has(wildcardPath(path))
330
+ );
331
+ }
332
+
333
+ /**
334
+ * Ensures platform-managed editorSchema sections exist. These sections are required
335
+ * by the strict contract validator when their sub-paths appear in controlOnlyPaths.
336
+ * Template developers must never need to add these manually.
337
+ */
338
+ function ensurePlatformSections(sections) {
339
+ // additionalPages: rendered visually by PlatformAdditionalPages component.
340
+ // label is editable; id and route are platform-controlled.
341
+ if (!sections.find((s) => s.id === 'additionalPages')) {
342
+ sections.push({
343
+ id: 'additionalPages',
344
+ path: 'additionalPages',
345
+ type: 'list',
346
+ label: 'Additional Pages',
347
+ minItems: 0,
348
+ maxItems: 20,
349
+ fields: [
350
+ { key: 'label', type: 'text', label: 'Page label', required: true },
351
+ { key: 'id', type: 'text', label: 'Page ID' },
352
+ { key: 'route', type: 'text', label: 'Page route' },
353
+ ],
354
+ });
355
+ }
356
+
357
+ // __fivoraIntake: AI intake data written by the platform. Never rendered in
358
+ // template HTML — always control-only. Section required so sub-paths can be
359
+ // declared in controlOnlyPaths without "unknown path" errors.
360
+ if (!sections.find((s) => s.id === '__fivoraIntake')) {
361
+ sections.push({
362
+ id: '__fivoraIntake',
363
+ path: '__fivoraIntake',
364
+ type: 'object',
365
+ label: 'AI Intake (Platform Managed)',
366
+ fields: [
367
+ { key: 'tone', type: 'text', label: 'Brand tone' },
368
+ { key: 'outputLanguage', type: 'text', label: 'Output language' },
369
+ { key: 'businessSummary', type: 'textarea', label: 'Business summary' },
370
+ { key: 'additionalBusinessDetails', type: 'textarea', label: 'Additional business details' },
371
+ { key: 'referenceWebsiteUrl', type: 'url', label: 'Reference website URL' },
372
+ { key: 'guidanceNotes', type: 'object', label: 'AI Guidance Notes' },
373
+ ],
374
+ });
375
+ }
318
376
  }
319
377
 
320
378
  function computeControlOnlyPaths(content, boundPaths, declared = []) {
@@ -322,6 +380,17 @@ function computeControlOnlyPaths(content, boundPaths, declared = []) {
322
380
  const bound = new Set([...(boundPaths || [])].map(wildcardPath));
323
381
  const controlOnly = new Set();
324
382
 
383
+ // Auto-inject all platform-contract paths that exist in site data content —
384
+ // templates never need to declare these manually.
385
+ for (const platformPath of PLATFORM_CONTRACT.platformControlledPaths || []) {
386
+ const canonical = canonicalizeMarkerPath(platformPath);
387
+ if (!canonical) continue;
388
+ const known = [...inventory.fieldPatterns, ...inventory.concreteFields].some(
389
+ (path) => wildcardPath(path) === wildcardPath(canonical)
390
+ );
391
+ if (known) controlOnly.add(canonical);
392
+ }
393
+
325
394
  for (const declaredPath of declared) {
326
395
  const canonical = canonicalizeMarkerPath(declaredPath);
327
396
  if (!canonical || !isAllowedControlOnly(canonical)) continue;
@@ -506,6 +575,14 @@ function buildSiteDataAndManifest({
506
575
  assignSectionPageKeys(editorSections, routes, markerRoutes);
507
576
  enrichSchemasFromContent(content, editorSections);
508
577
 
578
+ // Auto-inject platform-managed editorSchema sections if not already present.
579
+ // These sections are required so the validator accepts their controlOnlyPaths,
580
+ // but template developers should never need to add these manually.
581
+ ensurePlatformSections(editorSections);
582
+
583
+ // controlOnlyPaths: platform paths are auto-merged by platform-contract.json at
584
+ // validate/build time. We only emit template-specific paths here (currently none
585
+ // by default — templates add their own via controlOnlyPaths in fivora-template.json).
509
586
  const controlOnlyPaths = computeControlOnlyPaths(
510
587
  content,
511
588
  boundFieldPaths,
@@ -1046,11 +1046,78 @@ function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-u
1046
1046
  }
1047
1047
 
1048
1048
  ensureProviderInitialData(ast, jsonIdent);
1049
+ injectPlatformAdditionalPages(ast, providerImport);
1049
1050
  sanitizeDuplicateBindings(ast);
1050
1051
  const next = printSource(ast, code);
1051
1052
  return { code: next, updated: next !== code };
1052
1053
  }
1053
1054
 
1055
+ /**
1056
+ * Injects <PlatformAdditionalPages /> into the layout JSX tree after <main>.
1057
+ * This component handles all additionalPages visual-editing markers correctly
1058
+ * so template developers never need to know the complex nesting rules.
1059
+ *
1060
+ * The injected pattern looks like:
1061
+ * <main>{children}</main>
1062
+ * <PlatformAdditionalPages /> ← injected (inside the SiteDataProvider/body)
1063
+ *
1064
+ * If PlatformAdditionalPages is already in the source, it's a no-op.
1065
+ */
1066
+ function injectPlatformAdditionalPages(ast, providerImport) {
1067
+ if (!ast) return;
1068
+ // Already injected - skip
1069
+ let alreadyPresent = false;
1070
+ recast.types.visit(ast, {
1071
+ visitJSXIdentifier(pathNode) {
1072
+ if (pathNode.node.name === 'PlatformAdditionalPages') {
1073
+ alreadyPresent = true;
1074
+ return false;
1075
+ }
1076
+ this.traverse(pathNode);
1077
+ },
1078
+ });
1079
+ if (alreadyPresent) return;
1080
+
1081
+ // Inject after the first <main> element in any JSX tree
1082
+ let injected = false;
1083
+ recast.types.visit(ast, {
1084
+ visitJSXElement(pathNode) {
1085
+ if (injected) return false;
1086
+ const name = getJsxName(pathNode.node);
1087
+ if (name === 'main') {
1088
+ const parent = pathNode.parent;
1089
+ if (!parent || !Array.isArray(parent.node.children)) {
1090
+ this.traverse(pathNode);
1091
+ return;
1092
+ }
1093
+ const siblings = parent.node.children;
1094
+ const idx = siblings.indexOf(pathNode.node);
1095
+ if (idx === -1) {
1096
+ this.traverse(pathNode);
1097
+ return;
1098
+ }
1099
+ // Build <PlatformAdditionalPages />
1100
+ const platformElement = b.jsxElement(
1101
+ b.jsxOpeningElement(
1102
+ b.jsxIdentifier('PlatformAdditionalPages'),
1103
+ [],
1104
+ true // self-closing
1105
+ ),
1106
+ null,
1107
+ [],
1108
+ true
1109
+ );
1110
+ const newline = b.jsxText('\n ');
1111
+ siblings.splice(idx + 1, 0, newline, platformElement);
1112
+ ensureImport(ast, providerImport || '@deneb-ui/ui', ['PlatformAdditionalPages']);
1113
+ injected = true;
1114
+ return false;
1115
+ }
1116
+ this.traverse(pathNode);
1117
+ },
1118
+ });
1119
+ }
1120
+
1054
1121
  function ensureJsonModule(tsconfig) {
1055
1122
  if (!tsconfig || !tsconfig.compilerOptions) return { config: tsconfig, changed: false };
1056
1123
  if (tsconfig.compilerOptions.resolveJsonModule) return { config: tsconfig, changed: false };
@@ -0,0 +1,39 @@
1
+ {
2
+ "_comment": "Platform-managed field paths for Fivora templates. These are written by the Fivora platform (AI agent, site builder) and must NEVER require data-preview-field-path HTML markers. The CLI merges these into every template's controlOnlyPaths automatically — templates do not need to declare them.",
3
+ "version": 1,
4
+ "platformControlledPaths": [
5
+ "__fivoraIntake.tone",
6
+ "__fivoraIntake.outputLanguage",
7
+ "__fivoraIntake.businessSummary",
8
+ "__fivoraIntake.additionalBusinessDetails",
9
+ "__fivoraIntake.referenceWebsiteUrl",
10
+ "additionalPages[*].id",
11
+ "additionalPages[*].route",
12
+ "common.currency",
13
+ "common.whatsappNumber",
14
+ "common.whatsapp",
15
+ "common.email",
16
+ "common.address",
17
+ "common.contactNumber",
18
+ "common.openingHours",
19
+ "common.newsletterTitle",
20
+ "common.newsletterDescription",
21
+ "contact.contactNumber",
22
+ "contact.googleMapLink",
23
+ "contact.whatsapp",
24
+ "products[*].id",
25
+ "products[*].isAvailable",
26
+ "products[*].measurement",
27
+ "products[*].unit",
28
+ "testimonials[*].id",
29
+ "categories[*].slug"
30
+ ],
31
+ "platformSiteDataKeys": [
32
+ "__fivoraIntake",
33
+ "additionalPages"
34
+ ],
35
+ "forbiddenNextConfigKeys": [
36
+ "outputFileTracingRoot",
37
+ "experimental.outputFileTracingRoot"
38
+ ]
39
+ }