@deneb-ui/cli 2.0.32 → 2.0.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.32",
3
+ "version": "2.0.34",
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.32",
52
+ "@deneb-ui/core": "^2.0.34",
53
53
  "adm-zip": "^0.6.0",
54
54
  "recast": "^0.23.11"
55
55
  },
@@ -10,11 +10,57 @@ const { scanProject, buildDependencyGraph } = require('../scanner.cjs');
10
10
  const { analyzeFile } = require('../semantic.cjs');
11
11
  const { planTransformations } = require('../planner.cjs');
12
12
  const { applyFilePlan } = require('../transformer.cjs');
13
- const { buildFieldPath, inferFieldName } = require('../field-paths.cjs');
13
+ const { buildFieldPath, inferFieldName, isListActionCtaKey } = require('../field-paths.cjs');
14
+ const { enrichSchemasFromContent } = require('../manifest.cjs');
14
15
  const { runDenebArc } = require('../index.cjs');
15
16
  const { parseSource } = require('../ast.cjs');
16
17
  const { loadFingerprintBoost } = require('../learning.cjs');
17
18
 
19
+ test('field-paths recognizes list action CTA keys', () => {
20
+ assert.equal(isListActionCtaKey('preOrderCta'), true);
21
+ assert.equal(isListActionCtaKey('addToTrayCta'), true);
22
+ assert.equal(isListActionCtaKey('features'), false);
23
+ });
24
+
25
+ test('manifest enrichSchemasFromContent registers list CTA and paired directions fields', () => {
26
+ const sections = [
27
+ {
28
+ id: 'contact',
29
+ path: 'contact',
30
+ type: 'object',
31
+ label: 'Contact',
32
+ fields: [],
33
+ },
34
+ {
35
+ id: 'home',
36
+ path: 'home',
37
+ type: 'object',
38
+ label: 'Home',
39
+ fields: [],
40
+ },
41
+ ];
42
+ enrichSchemasFromContent(
43
+ {
44
+ contact: {
45
+ directionsLabel: 'Get Directions',
46
+ directionsUrl: 'https://maps.example',
47
+ },
48
+ home: {
49
+ demoPreOrderCta: [{ buttonLabel: 'Order', buttonUrl: 'https://wa.me/123' }],
50
+ },
51
+ },
52
+ sections
53
+ );
54
+ const contactFields = sections.find((s) => s.id === 'contact').fields;
55
+ assert.ok(contactFields.some((f) => f.key === 'directionsUrl' && f.type === 'url'));
56
+ assert.ok(contactFields.some((f) => f.key === 'directionsLabel'));
57
+ const homeFields = sections.find((s) => s.id === 'home').fields;
58
+ const list = homeFields.find((f) => f.key === 'demoPreOrderCta');
59
+ assert.ok(list && list.type === 'list');
60
+ assert.ok(list.fields.some((f) => f.key === 'buttonLabel'));
61
+ assert.ok(list.fields.some((f) => f.key === 'buttonUrl' && f.type === 'url'));
62
+ });
63
+
18
64
  test('unseen fingerprints do not change planner confidence', () => {
19
65
  const hint = loadFingerprintBoost(null);
20
66
  assert.equal(hint.boost, 0);
@@ -109,6 +155,8 @@ test('AST transformer preserves className and uses nullish fallbacks', () => {
109
155
  assert.match(result.code, /data-preview-field-path=/);
110
156
  assert.match(result.code, /\?\?/);
111
157
  assert.match(result.code, /<span[\s\S]*data-preview-field-path="/);
158
+ assert.match(result.code, /<span[^>]*hidden[^>]*data-preview-field-path="/);
159
+ assert.match(result.code, /data-preview-static="action-link"/);
112
160
  assert.doesNotMatch(result.code, /'use client'/);
113
161
  assert.match(result.code, /site-data\.json|@\/data\/site-data\.json/);
114
162
  assert.match(result.code, /data-preview-style-target=/);
@@ -141,6 +141,14 @@ function humanLabel(fieldPath) {
141
141
  .trim();
142
142
  }
143
143
 
144
+ function isListActionCtaKey(key) {
145
+ return /Cta$/i.test(String(key || ''));
146
+ }
147
+
148
+ function listActionCtaItemFieldNames() {
149
+ return ['buttonLabel', 'buttonUrl'];
150
+ }
151
+
144
152
  function classifyFieldType(kind, value) {
145
153
  if (kind === 'image') return 'image';
146
154
  if (kind === 'url') return 'url';
@@ -162,4 +170,6 @@ module.exports = {
162
170
  buildFieldPath,
163
171
  humanLabel,
164
172
  classifyFieldType,
173
+ isListActionCtaKey,
174
+ listActionCtaItemFieldNames,
165
175
  };
@@ -59,7 +59,7 @@ function upsertSchemaField(sections, fieldPath, fieldType, label, required = fal
59
59
  if (!existing) {
60
60
  fields.push({
61
61
  key,
62
- type: fieldType === 'url' ? 'text' : fieldType === 'textarea' ? 'textarea' : fieldType,
62
+ type: fieldType === 'url' ? 'url' : fieldType === 'textarea' ? 'textarea' : fieldType === 'phone' ? 'tel' : fieldType,
63
63
  label: label || humanLabel(key),
64
64
  ...(required ? { required: true } : {}),
65
65
  });
@@ -119,12 +119,77 @@ function upsertSchemaList(sections, listPath, itemFields, items) {
119
119
  maxItems: Math.max((items || []).length, 12),
120
120
  fields: (itemFields || []).map((field) => ({
121
121
  key: field.key,
122
- type: field.type === 'url' ? 'url' : field.type === 'image' ? 'image' : 'text',
122
+ type:
123
+ field.type === 'url'
124
+ ? 'url'
125
+ : field.type === 'image'
126
+ ? 'image'
127
+ : field.type === 'textarea'
128
+ ? 'textarea'
129
+ : field.type === 'tel' || field.type === 'phone'
130
+ ? 'tel'
131
+ : field.type === 'email'
132
+ ? 'email'
133
+ : field.type || 'text',
123
134
  label: humanLabel(field.key),
124
135
  })),
125
136
  });
126
137
  }
127
138
 
139
+ const LIST_ACTION_CTA_ITEM_FIELDS = [
140
+ { key: 'buttonLabel', type: 'text' },
141
+ { key: 'buttonUrl', type: 'url' },
142
+ ];
143
+
144
+ function isListActionCtaKey(key) {
145
+ return /Cta$/i.test(String(key || ''));
146
+ }
147
+
148
+ /**
149
+ * Walks merged site-data content and registers schemas ARC learns from merchant patterns:
150
+ * list CTAs (`*Cta` with buttonLabel/buttonUrl) and paired *Label/*Url siblings.
151
+ */
152
+ function enrichSchemasFromContent(content, sections) {
153
+ if (!content || typeof content !== 'object') return;
154
+
155
+ function walk(node, prefix) {
156
+ if (!node || typeof node !== 'object') return;
157
+ if (Array.isArray(node)) {
158
+ const listKey = prefix.split('.').pop() || '';
159
+ if (isListActionCtaKey(listKey)) {
160
+ const sample = node[0];
161
+ if (sample && typeof sample === 'object' && ('buttonLabel' in sample || 'buttonUrl' in sample)) {
162
+ upsertSchemaList(sections, prefix, LIST_ACTION_CTA_ITEM_FIELDS, node);
163
+ }
164
+ }
165
+ node.forEach((item, idx) => {
166
+ if (item && typeof item === 'object') walk(item, `${prefix}[${idx}]`);
167
+ });
168
+ return;
169
+ }
170
+
171
+ for (const [key, value] of Object.entries(node)) {
172
+ const nextPath = prefix ? `${prefix}.${key}` : key;
173
+ if (Array.isArray(value) && isListActionCtaKey(key)) {
174
+ const sample = value[0];
175
+ if (sample && typeof sample === 'object' && ('buttonLabel' in sample || 'buttonUrl' in sample)) {
176
+ upsertSchemaList(sections, nextPath, LIST_ACTION_CTA_ITEM_FIELDS, value);
177
+ }
178
+ } else if (typeof value === 'string' && /Url$/i.test(key)) {
179
+ upsertSchemaField(sections, nextPath, 'url', humanLabel(key));
180
+ const labelKey = key.replace(/Url$/i, 'Label');
181
+ if (Object.prototype.hasOwnProperty.call(node, labelKey)) {
182
+ upsertSchemaField(sections, `${prefix}.${labelKey}`, 'text', humanLabel(labelKey));
183
+ }
184
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
185
+ walk(value, nextPath);
186
+ }
187
+ }
188
+ }
189
+
190
+ walk(content, '');
191
+ }
192
+
128
193
  function collectFieldsFromPlan(plan) {
129
194
  const fields = [];
130
195
  for (const file of plan.files || []) {
@@ -362,6 +427,7 @@ function buildSiteDataAndManifest({
362
427
  siteData.template.structure.pages = routes.map((p) => p.id);
363
428
 
364
429
  assignSectionPageKeys(editorSections, routes, markerRoutes);
430
+ enrichSchemasFromContent(content, editorSections);
365
431
 
366
432
  const controlOnlyPaths = computeControlOnlyPaths(
367
433
  content,
@@ -435,4 +501,6 @@ module.exports = {
435
501
  computeControlOnlyPaths,
436
502
  assignSectionPageKeys,
437
503
  upsertSchemaList,
504
+ enrichSchemasFromContent,
505
+ isListActionCtaKey,
438
506
  };
@@ -126,13 +126,42 @@ function splitActionChildren(node, labelField, labelFallback) {
126
126
  node.children = nextChildren;
127
127
  }
128
128
 
129
+ function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
130
+ const urlParts = urlField.split('.');
131
+ const hiddenUrl = b.jsxElement(
132
+ b.jsxOpeningElement(
133
+ b.jsxIdentifier('span'),
134
+ [
135
+ b.jsxAttribute(b.jsxIdentifier('hidden')),
136
+ b.jsxAttribute(b.jsxIdentifier('aria-hidden'), b.stringLiteral('true')),
137
+ jsxPreviewAttr(urlField),
138
+ ],
139
+ false
140
+ ),
141
+ b.jsxClosingElement(b.jsxIdentifier('span')),
142
+ [b.jsxExpressionContainer(siteDataBinding(urlParts, fallback, 'url'))],
143
+ false
144
+ );
145
+ pathNode.insertAfter(hiddenUrl);
146
+ }
147
+
129
148
  function applyTransformToElement(pathNode, transform) {
130
149
  const node = pathNode.node;
131
150
  if (transform.operation === 'split-action-contract') {
132
151
  const urlParts = transform.urlField.split('.');
133
152
  replaceAttrValue(node, 'href', siteDataBinding(urlParts, transform.fallback, 'url'));
134
- ensurePreviewPath(node, transform.urlField);
153
+ if (!hasJsxAttribute(node, 'data-preview-static')) {
154
+ node.openingElement.attributes.push(
155
+ b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-link'))
156
+ );
157
+ }
158
+ if (hasJsxAttribute(node, 'data-preview-field-path')) {
159
+ node.openingElement.attributes = node.openingElement.attributes.filter(
160
+ (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
161
+ );
162
+ }
135
163
  splitActionChildren(node, transform.labelField, transform.labelFallback || '');
164
+ wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
136
165
  return;
137
166
  }
138
167
  if (transform.operation === 'extract-url') {