@deneb-ui/cli 2.0.32 → 2.0.33
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 +2 -2
- package/src/arc/__tests__/arc.test.cjs +49 -1
- package/src/arc/field-paths.cjs +10 -0
- package/src/arc/manifest.cjs +70 -2
- package/src/arc/transformer.cjs +30 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.33",
|
|
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.
|
|
52
|
+
"@deneb-ui/core": "^2.0.33",
|
|
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=/);
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -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
|
};
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -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' ? '
|
|
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:
|
|
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
|
};
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -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
|
-
|
|
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') {
|