@deneb-ui/cli 2.0.49 → 2.0.51
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 +15 -5
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +465 -1
- package/src/arc/adapters.cjs +24 -1
- package/src/arc/ai-agent.cjs +6 -0
- package/src/arc/ai-evaluator.cjs +450 -0
- package/src/arc/ai-prompts.cjs +1 -0
- package/src/arc/ast.cjs +11 -3
- package/src/arc/field-paths.cjs +4 -0
- package/src/arc/index.cjs +30 -3
- package/src/arc/learning.cjs +31 -0
- package/src/arc/planner.cjs +18 -7
- package/src/arc/printer.cjs +20 -0
- package/src/arc/semantic.cjs +168 -37
- package/src/arc/style-candidates.cjs +2 -1
- package/src/arc/transformer.cjs +29 -1
package/src/arc/planner.cjs
CHANGED
|
@@ -9,7 +9,7 @@ const { appendStyleBindTransforms } = require('./style-candidates.cjs');
|
|
|
9
9
|
function recipeBoost(candidate, recipe) {
|
|
10
10
|
if (!recipe) return 0;
|
|
11
11
|
let boost = 0;
|
|
12
|
-
if (recipe.actionRules?.splitActionAndLabel && candidate.operation === 'split-action-contract') boost += 0.03;
|
|
12
|
+
if (recipe.actionRules?.splitActionAndLabel && (candidate.operation === 'split-action-contract' || candidate.operation === 'form-submit-action')) boost += 0.03;
|
|
13
13
|
const keywords = recipe.signatures?.keywords || [];
|
|
14
14
|
const hay = `${candidate.tag} ${candidate.value || ''} ${candidate.label || ''} ${candidate.file || ''}`.toLowerCase();
|
|
15
15
|
if (keywords.some((kw) => hay.includes(String(kw).toLowerCase()))) boost += 0.02;
|
|
@@ -66,8 +66,8 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const extra = { ...(candidate.extra || {}) };
|
|
69
|
-
if (candidate.operation === 'split-action-contract') {
|
|
70
|
-
extra.action = extra.action || classifyHref(candidate.value);
|
|
69
|
+
if (candidate.operation === 'split-action-contract' || candidate.operation === 'form-submit-action') {
|
|
70
|
+
extra.action = extra.action || (candidate.operation === 'form-submit-action' ? 'form-submit' : classifyHref(candidate.value));
|
|
71
71
|
extra.paired = true;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -107,7 +107,7 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
if (candidate.operation === 'split-action-contract') {
|
|
110
|
+
if (candidate.operation === 'split-action-contract' || candidate.operation === 'form-submit-action') {
|
|
111
111
|
const urlName = inferFieldName('url', candidate.tag, candidate.label, extra);
|
|
112
112
|
const labelName = inferFieldName('label', candidate.tag, candidate.label, { ...extra, paired: true });
|
|
113
113
|
const actionSection = sectionForAction(scope, section, extra);
|
|
@@ -117,6 +117,9 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
117
117
|
transform.labelField = uniquePath(usedPaths, sibling.join('.'));
|
|
118
118
|
transform.fieldType = 'url';
|
|
119
119
|
transform.labelFieldType = 'text';
|
|
120
|
+
if (candidate.operation === 'form-submit-action') {
|
|
121
|
+
transform.formContext = true;
|
|
122
|
+
}
|
|
120
123
|
} else if (candidate.operation === 'extract-url') {
|
|
121
124
|
transform.field = buildFieldPath({
|
|
122
125
|
scope,
|
|
@@ -177,8 +180,16 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
177
180
|
}));
|
|
178
181
|
transform.items = candidate.value.map((item) => item.value);
|
|
179
182
|
transform.itemParam = extra.itemParam;
|
|
180
|
-
transform.
|
|
181
|
-
|
|
183
|
+
if (!transform.itemFields.length && extra.objectItems && candidate.value?.length > 0) {
|
|
184
|
+
const sample = candidate.value[0]?.value || {};
|
|
185
|
+
transform.itemFields = Object.keys(sample)
|
|
186
|
+
.filter((k) => typeof sample[k] === 'string' || typeof sample[k] === 'number')
|
|
187
|
+
.map((k) => ({
|
|
188
|
+
key: k,
|
|
189
|
+
type: typeof sample[k] === 'number' ? 'number' : /image|photo|avatar/i.test(k) ? 'image' : /url|link/i.test(k) ? 'url' : 'text',
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
if (!transform.itemFields.length || !extra.objectItems || extra.hasComponentRef) transform.decision = 'skip';
|
|
182
193
|
} else {
|
|
183
194
|
transform.field = buildFieldPath({
|
|
184
195
|
scope,
|
|
@@ -230,7 +241,7 @@ function sectionForAction(scope, section, extra) {
|
|
|
230
241
|
if (scope === 'common' && (extra.social || ['instagram', 'facebook', 'twitter', 'tiktok', 'youtube', 'linkedin'].includes(extra.action))) {
|
|
231
242
|
return 'footer';
|
|
232
243
|
}
|
|
233
|
-
if (['whatsapp', 'phone', 'email', 'directions', 'location'].includes(extra.action)) {
|
|
244
|
+
if (['whatsapp', 'phone', 'email', 'directions', 'location', 'form-submit'].includes(extra.action)) {
|
|
234
245
|
return section || 'contact';
|
|
235
246
|
}
|
|
236
247
|
if (extra.action === 'shop') {
|
package/src/arc/printer.cjs
CHANGED
|
@@ -202,6 +202,22 @@ function printAiSummary(adapted, skipped, totalTokens) {
|
|
|
202
202
|
console.log(`\n ${C.cyan}⚡${C.reset} AI Agent Summary: ${C.green}${adapted} adapted${C.reset}, ${C.yellow}${skipped} skipped${C.reset}, ~${totalTokens.toLocaleString()} tokens used`);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
function printAiEvaluatorStart() {
|
|
206
|
+
console.log(`\n ${C.cyan}⚡${C.reset} ${C.bold}AI Evaluator:${C.reset} Running pre-flight runtime integrity audit...`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function printAiEvaluatorPass(message) {
|
|
210
|
+
console.log(` ${C.green}✓${C.reset} ${message}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function printAiEvaluatorHealed(message) {
|
|
214
|
+
console.log(` ${C.cyan}🔧${C.reset} ${C.bold}Auto-healed:${C.reset} ${message}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function printAiEvaluatorIssue(message) {
|
|
218
|
+
console.log(` ${C.yellow}⚠${C.reset} ${message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
module.exports = {
|
|
206
222
|
printBanner,
|
|
207
223
|
printProfile,
|
|
@@ -223,6 +239,10 @@ module.exports = {
|
|
|
223
239
|
printAiSkipped,
|
|
224
240
|
printAiPr,
|
|
225
241
|
printAiSummary,
|
|
242
|
+
printAiEvaluatorStart,
|
|
243
|
+
printAiEvaluatorPass,
|
|
244
|
+
printAiEvaluatorHealed,
|
|
245
|
+
printAiEvaluatorIssue,
|
|
226
246
|
ok,
|
|
227
247
|
warn,
|
|
228
248
|
info,
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -23,6 +23,8 @@ const {
|
|
|
23
23
|
TEXT_TAGS,
|
|
24
24
|
ACTION_TAGS,
|
|
25
25
|
IMAGE_TAGS,
|
|
26
|
+
FORM_INPUT_TAGS,
|
|
27
|
+
FORM_CONTAINER_TAGS,
|
|
26
28
|
DECORATIVE_TAGS,
|
|
27
29
|
} = require('./adapters.cjs');
|
|
28
30
|
const { shortHash } = require('./fs-utils.cjs');
|
|
@@ -49,35 +51,53 @@ function isStaticSkipText(text) {
|
|
|
49
51
|
return false;
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function unwrapTypeCasts(node) {
|
|
55
|
+
let curr = node;
|
|
56
|
+
while (
|
|
57
|
+
curr &&
|
|
58
|
+
(curr.type === 'TSAsExpression' ||
|
|
59
|
+
curr.type === 'TSTypeAssertion' ||
|
|
60
|
+
curr.type === 'TypeCastExpression' ||
|
|
61
|
+
curr.type === 'TSNonNullExpression')
|
|
62
|
+
) {
|
|
63
|
+
curr = curr.expression;
|
|
64
|
+
}
|
|
65
|
+
return curr;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
function collectStringBindings(ast) {
|
|
53
69
|
const bindings = new Map();
|
|
54
70
|
recast.types.visit(ast, {
|
|
55
71
|
visitVariableDeclarator(pathNode) {
|
|
56
72
|
const node = pathNode.node;
|
|
57
73
|
if (node.id && node.id.type === 'Identifier' && node.init) {
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
const init = unwrapTypeCasts(node.init);
|
|
75
|
+
if (init.type === 'StringLiteral' || (init.type === 'Literal' && typeof init.value === 'string')) {
|
|
76
|
+
bindings.set(node.id.name, String(init.value));
|
|
60
77
|
}
|
|
61
|
-
if (
|
|
78
|
+
if (init.type === 'ArrayExpression') {
|
|
62
79
|
const items = [];
|
|
63
|
-
|
|
64
|
-
for (const el of node.init.elements || []) {
|
|
80
|
+
for (const el of init.elements || []) {
|
|
65
81
|
if (!el) continue;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
const inner = unwrapTypeCasts(el);
|
|
83
|
+
if (inner.type === 'StringLiteral' || (inner.type === 'Literal' && typeof inner.value === 'string')) {
|
|
84
|
+
items.push({ type: 'string', value: String(inner.value) });
|
|
85
|
+
} else if (inner.type === 'ObjectExpression') {
|
|
86
|
+
const obj = objectLiteralToPlain(inner);
|
|
87
|
+
if (obj && Object.keys(obj).length > 0) {
|
|
88
|
+
items.push({ type: 'object', value: obj });
|
|
89
|
+
}
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
|
-
if (
|
|
92
|
+
if (items.length > 0) {
|
|
93
|
+
bindings.set(node.id.name, { kind: 'array', items });
|
|
94
|
+
}
|
|
77
95
|
}
|
|
78
|
-
if (
|
|
79
|
-
const obj = objectLiteralToPlain(
|
|
80
|
-
if (obj
|
|
96
|
+
if (init.type === 'ObjectExpression') {
|
|
97
|
+
const obj = objectLiteralToPlain(init);
|
|
98
|
+
if (obj && Object.keys(obj).length > 0) {
|
|
99
|
+
bindings.set(node.id.name, { kind: 'object', value: obj });
|
|
100
|
+
}
|
|
81
101
|
}
|
|
82
102
|
}
|
|
83
103
|
this.traverse(pathNode);
|
|
@@ -90,18 +110,57 @@ function objectLiteralToPlain(node) {
|
|
|
90
110
|
if (!node || node.type !== 'ObjectExpression') return null;
|
|
91
111
|
const out = {};
|
|
92
112
|
for (const prop of node.properties || []) {
|
|
93
|
-
if (prop.type !== 'ObjectProperty' && prop.type !== 'Property')
|
|
113
|
+
if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') continue;
|
|
94
114
|
const key = prop.key && (prop.key.name || prop.key.value);
|
|
95
|
-
if (!key || prop.computed)
|
|
96
|
-
const val = prop.value;
|
|
115
|
+
if (!key || prop.computed) continue;
|
|
116
|
+
const val = unwrapTypeCasts(prop.value);
|
|
117
|
+
if (!val) continue;
|
|
118
|
+
|
|
97
119
|
if (val.type === 'StringLiteral' || (val.type === 'Literal' && typeof val.value === 'string')) {
|
|
98
120
|
out[key] = String(val.value);
|
|
99
121
|
} else if (val.type === 'NumericLiteral' || (val.type === 'Literal' && typeof val.value === 'number')) {
|
|
100
122
|
out[key] = val.value;
|
|
101
123
|
} else if (val.type === 'BooleanLiteral' || (val.type === 'Literal' && typeof val.value === 'boolean')) {
|
|
102
124
|
out[key] = val.value;
|
|
103
|
-
} else {
|
|
104
|
-
|
|
125
|
+
} else if (val.type === 'NullLiteral' || (val.type === 'Literal' && val.value === null)) {
|
|
126
|
+
out[key] = null;
|
|
127
|
+
} else if (val.type === 'TemplateLiteral' && (!val.expressions || val.expressions.length === 0)) {
|
|
128
|
+
out[key] = val.quasis?.map((q) => q.value?.cooked || q.value?.raw || '').join('') || '';
|
|
129
|
+
} else if (val.type === 'Identifier') {
|
|
130
|
+
if (/^[A-Z]/.test(val.name)) {
|
|
131
|
+
// Component references (e.g. icon: Truck, Icon: ShieldCheck) are not merchant content
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
out[key] = val.name;
|
|
135
|
+
} else if (val.type === 'UnaryExpression' && val.argument) {
|
|
136
|
+
if (val.operator === '-' && (val.argument.type === 'NumericLiteral' || typeof val.argument.value === 'number')) {
|
|
137
|
+
out[key] = -val.argument.value;
|
|
138
|
+
} else if (val.operator === '!' && (val.argument.type === 'BooleanLiteral' || typeof val.argument.value === 'boolean')) {
|
|
139
|
+
out[key] = !val.argument.value;
|
|
140
|
+
}
|
|
141
|
+
} else if (val.type === 'ArrayExpression') {
|
|
142
|
+
const arr = [];
|
|
143
|
+
for (const el of val.elements || []) {
|
|
144
|
+
if (!el) continue;
|
|
145
|
+
const inner = unwrapTypeCasts(el);
|
|
146
|
+
if (!inner) continue;
|
|
147
|
+
if (inner.type === 'StringLiteral' || (inner.type === 'Literal' && typeof inner.value === 'string')) {
|
|
148
|
+
arr.push(String(inner.value));
|
|
149
|
+
} else if (inner.type === 'NumericLiteral' || (inner.type === 'Literal' && typeof inner.value === 'number')) {
|
|
150
|
+
arr.push(inner.value);
|
|
151
|
+
} else if (inner.type === 'BooleanLiteral' || (inner.type === 'Literal' && typeof inner.value === 'boolean')) {
|
|
152
|
+
arr.push(inner.value);
|
|
153
|
+
} else if (inner.type === 'ObjectExpression') {
|
|
154
|
+
const nestedObj = objectLiteralToPlain(inner);
|
|
155
|
+
if (nestedObj) arr.push(nestedObj);
|
|
156
|
+
} else if (inner.type === 'Identifier') {
|
|
157
|
+
arr.push(inner.name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
out[key] = arr;
|
|
161
|
+
} else if (val.type === 'ObjectExpression') {
|
|
162
|
+
const nestedObj = objectLiteralToPlain(val);
|
|
163
|
+
if (nestedObj) out[key] = nestedObj;
|
|
105
164
|
}
|
|
106
165
|
}
|
|
107
166
|
return out;
|
|
@@ -262,16 +321,47 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
262
321
|
usage.set(property, role);
|
|
263
322
|
}
|
|
264
323
|
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
if (expr
|
|
268
|
-
|
|
324
|
+
function findItemMemberProperties(expr) {
|
|
325
|
+
const props = [];
|
|
326
|
+
if (!expr) return props;
|
|
327
|
+
if (expr.type === 'MemberExpression' && !expr.computed) {
|
|
328
|
+
if (expr.object?.type === 'Identifier' && expr.object.name === itemParam) {
|
|
329
|
+
if (expr.property?.name) props.push(expr.property.name);
|
|
330
|
+
} else if (expr.object?.type === 'MemberExpression') {
|
|
331
|
+
const sub = findItemMemberProperties(expr.object);
|
|
332
|
+
if (sub.length > 0 && expr.property?.name) {
|
|
333
|
+
props.push(expr.property.name);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else if (expr.type === 'LogicalExpression' || expr.type === 'BinaryExpression') {
|
|
337
|
+
props.push(...findItemMemberProperties(expr.left));
|
|
338
|
+
props.push(...findItemMemberProperties(expr.right));
|
|
339
|
+
} else if (expr.type === 'ConditionalExpression') {
|
|
340
|
+
props.push(...findItemMemberProperties(expr.test));
|
|
341
|
+
props.push(...findItemMemberProperties(expr.consequent));
|
|
342
|
+
props.push(...findItemMemberProperties(expr.alternate));
|
|
343
|
+
} else if (expr.type === 'TemplateLiteral') {
|
|
344
|
+
for (const sub of expr.expressions || []) {
|
|
345
|
+
props.push(...findItemMemberProperties(sub));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return props;
|
|
269
349
|
}
|
|
270
350
|
|
|
351
|
+
let usesItemAsComponent = false;
|
|
271
352
|
recast.types.visit(callback, {
|
|
353
|
+
visitJSXOpeningElement(pathNode) {
|
|
354
|
+
const name = pathNode.node.name;
|
|
355
|
+
if (name?.type === 'JSXMemberExpression') {
|
|
356
|
+
if (name.object?.type === 'Identifier' && name.object.name === itemParam) {
|
|
357
|
+
usesItemAsComponent = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.traverse(pathNode);
|
|
361
|
+
},
|
|
272
362
|
visitJSXExpressionContainer(pathNode) {
|
|
273
|
-
const
|
|
274
|
-
|
|
363
|
+
const properties = findItemMemberProperties(pathNode.node.expression);
|
|
364
|
+
for (const property of properties) {
|
|
275
365
|
const parent = pathNode.parent?.node || pathNode.parent?.value;
|
|
276
366
|
if (parent?.type === 'JSXAttribute') {
|
|
277
367
|
const attribute = parent.name?.name;
|
|
@@ -290,7 +380,7 @@ function collectItemFieldUsage(callback, itemParam) {
|
|
|
290
380
|
},
|
|
291
381
|
});
|
|
292
382
|
|
|
293
|
-
return usage;
|
|
383
|
+
return { usage, usesItemAsComponent };
|
|
294
384
|
}
|
|
295
385
|
|
|
296
386
|
function classNameOf(node) {
|
|
@@ -304,7 +394,7 @@ function confidenceFor(kind, extras = {}) {
|
|
|
304
394
|
if (extras.icon) return 0.1;
|
|
305
395
|
if (kind === 'url' && extras.action === 'whatsapp') return 0.96;
|
|
306
396
|
if (kind === 'url' && extras.social) return 0.93;
|
|
307
|
-
if (kind === 'split-action-contract') return 0.94;
|
|
397
|
+
if (kind === 'split-action-contract' || kind === 'form-submit-action') return 0.94;
|
|
308
398
|
if (kind === 'text' && HEADING_TAGS.has(extras.tag)) return 0.95;
|
|
309
399
|
if (kind === 'text' && extras.tag === 'p') return 0.9;
|
|
310
400
|
if (kind === 'image') return 0.88;
|
|
@@ -474,14 +564,47 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
474
564
|
});
|
|
475
565
|
}
|
|
476
566
|
|
|
567
|
+
const insideForm = parents.some((p) => FORM_CONTAINER_TAGS.has(p));
|
|
568
|
+
|
|
477
569
|
const actionableHref = href || (name === 'Button' ? getJsxAttributeLiteral(node, 'href') : null);
|
|
478
570
|
const innerText = textInfo.dynamic ? '' : textInfo.text;
|
|
479
571
|
const typeAttr = getJsxAttributeLiteral(node, 'type');
|
|
480
572
|
const isSubmit = typeAttr === 'submit';
|
|
481
573
|
|
|
482
|
-
// Check for action intent via classifyActionIntent (covers WhatsApp, Call/Phone, Directions, Location, Shop, Email)
|
|
483
|
-
const actionIntent = !
|
|
484
|
-
|
|
574
|
+
// Check for action intent via classifyActionIntent (covers Form Submit, WhatsApp, Call/Phone, Directions, Location, Shop, Email)
|
|
575
|
+
const actionIntent = !apiOwned ? classifyActionIntent(innerText, actionableHref) : null;
|
|
576
|
+
|
|
577
|
+
const isFormSubmit =
|
|
578
|
+
!apiOwned &&
|
|
579
|
+
(ACTION_TAGS.has(name) || isLikelyCtaClass(className)) &&
|
|
580
|
+
(isSubmit ||
|
|
581
|
+
(insideForm && (actionIntent?.action === 'form-submit' || (innerText && !actionableHref))) ||
|
|
582
|
+
actionIntent?.action === 'form-submit');
|
|
583
|
+
|
|
584
|
+
if (isFormSubmit && innerText && !textInfo.dynamic) {
|
|
585
|
+
usedLocs.add(loc);
|
|
586
|
+
const resolvedHref = actionableHref || (actionIntent ? actionIntent.defaultUrl : 'https://wa.me/1234567890');
|
|
587
|
+
candidates.push({
|
|
588
|
+
...baseMeta,
|
|
589
|
+
kind: 'form-submit-action',
|
|
590
|
+
operation: 'form-submit-action',
|
|
591
|
+
value: resolvedHref,
|
|
592
|
+
label: innerText,
|
|
593
|
+
extra: {
|
|
594
|
+
action: 'form-submit',
|
|
595
|
+
external: true,
|
|
596
|
+
insideForm,
|
|
597
|
+
isSubmit,
|
|
598
|
+
},
|
|
599
|
+
confidence: confidenceFor('form-submit-action'),
|
|
600
|
+
reason: 'form-submit-whatsapp-dispatch',
|
|
601
|
+
fingerprint: fingerprintCandidate({ tag: name, kind: 'form-submit-action', action: 'form-submit' }),
|
|
602
|
+
});
|
|
603
|
+
this.traverse(pathNode);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const isAction = !isSubmit && (Boolean(actionableHref) || Boolean(actionIntent)) && (ACTION_TAGS.has(name) || isLikelyCtaClass(className));
|
|
485
608
|
|
|
486
609
|
if (isAction && (actionableHref || actionIntent)) {
|
|
487
610
|
const action = actionIntent ? actionIntent.action : classifyHref(actionableHref);
|
|
@@ -592,13 +715,14 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
592
715
|
bindings.get(mapInfo.objectName)?.kind === 'array'
|
|
593
716
|
) {
|
|
594
717
|
const arr = bindings.get(mapInfo.objectName);
|
|
595
|
-
const itemUsage = collectItemFieldUsage(mapInfo.callback, mapInfo.itemParam);
|
|
718
|
+
const { usage: itemUsage, usesItemAsComponent } = collectItemFieldUsage(mapInfo.callback, mapInfo.itemParam);
|
|
596
719
|
const objectItems = arr.items.every((item) => item.type === 'object');
|
|
597
720
|
const boundProperties = [...itemUsage.keys()];
|
|
598
721
|
const convertible =
|
|
722
|
+
!usesItemAsComponent &&
|
|
599
723
|
objectItems &&
|
|
600
724
|
boundProperties.length > 0 &&
|
|
601
|
-
arr.items.
|
|
725
|
+
arr.items.some((item) => boundProperties.some((key) => key in item.value));
|
|
602
726
|
|
|
603
727
|
candidates.push({
|
|
604
728
|
...baseMeta,
|
|
@@ -612,11 +736,18 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
612
736
|
indexParam: mapInfo.indexParam,
|
|
613
737
|
itemFields: [...itemUsage.entries()].map(([key, role]) => ({ key, role })),
|
|
614
738
|
objectItems,
|
|
739
|
+
hasComponentRef: usesItemAsComponent,
|
|
615
740
|
},
|
|
616
741
|
confidence: convertible
|
|
617
742
|
? confidenceFor('collection', { staticCollection: true })
|
|
618
|
-
:
|
|
619
|
-
|
|
743
|
+
: usesItemAsComponent
|
|
744
|
+
? 0.2
|
|
745
|
+
: 0.82,
|
|
746
|
+
reason: usesItemAsComponent
|
|
747
|
+
? 'collection-holds-component-ref'
|
|
748
|
+
: convertible
|
|
749
|
+
? 'static-array-map'
|
|
750
|
+
: 'collection-shape-partial',
|
|
620
751
|
fingerprint: fingerprintCandidate({ tag: name, kind: 'collection', size: arr.items.length }),
|
|
621
752
|
});
|
|
622
753
|
}
|
|
@@ -5,6 +5,7 @@ const BUTTON_TAGS = new Set(['button', 'Button', 'CTAButton']);
|
|
|
5
5
|
|
|
6
6
|
function inferStyleKind(transform) {
|
|
7
7
|
if (transform.operation === 'collection-conversion') return 'grid';
|
|
8
|
+
if (transform.operation === 'form-submit-action') return 'button';
|
|
8
9
|
if (transform.operation === 'split-action-contract') return 'text';
|
|
9
10
|
if (BUTTON_TAGS.has(transform.tag) && transform.operation === 'extract-text') return 'button';
|
|
10
11
|
if (TEXT_OPS.has(transform.operation)) return 'text';
|
|
@@ -14,7 +15,7 @@ function inferStyleKind(transform) {
|
|
|
14
15
|
function stylePathFor(transform, kind) {
|
|
15
16
|
if (kind === 'grid' && transform.listField) return `${transform.listField}.grid`;
|
|
16
17
|
if (kind === 'card' && transform.listField) return `${transform.listField}[*].card`;
|
|
17
|
-
if (transform.operation === 'split-action-contract') return transform.labelField;
|
|
18
|
+
if (transform.operation === 'split-action-contract' || transform.operation === 'form-submit-action') return transform.labelField;
|
|
18
19
|
return transform.field || transform.labelField || null;
|
|
19
20
|
}
|
|
20
21
|
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
b,
|
|
25
25
|
} = require('./ast.cjs');
|
|
26
26
|
const { toPosix } = require('./fs-utils.cjs');
|
|
27
|
+
const { BROAD_CONTENT_CONTAINERS } = require('./fivora-contract.cjs');
|
|
27
28
|
|
|
28
29
|
function findElementByLoc(ast, loc) {
|
|
29
30
|
let found = null;
|
|
@@ -151,6 +152,27 @@ function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
|
|
|
151
152
|
|
|
152
153
|
function applyTransformToElement(pathNode, transform) {
|
|
153
154
|
const node = pathNode.node;
|
|
155
|
+
if (transform.operation === 'form-submit-action') {
|
|
156
|
+
const tagName = getJsxName(node);
|
|
157
|
+
if (!hasJsxAttribute(node, 'type') && tagName === 'button') {
|
|
158
|
+
node.openingElement.attributes.push(
|
|
159
|
+
b.jsxAttribute(b.jsxIdentifier('type'), b.stringLiteral('submit'))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!hasJsxAttribute(node, 'data-preview-static')) {
|
|
163
|
+
node.openingElement.attributes.push(
|
|
164
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-button'))
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
168
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
169
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
173
|
+
wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
154
176
|
if (transform.operation === 'split-action-contract') {
|
|
155
177
|
const tagName = getJsxName(node);
|
|
156
178
|
if (tagName === 'button') {
|
|
@@ -222,6 +244,11 @@ function applyTransformToElement(pathNode, transform) {
|
|
|
222
244
|
return;
|
|
223
245
|
}
|
|
224
246
|
if (transform.operation === 'extract-text') {
|
|
247
|
+
const tagName = getJsxName(node);
|
|
248
|
+
if (BROAD_CONTENT_CONTAINERS.has(tagName)) {
|
|
249
|
+
wrapLiteralTextChildren(node, transform.field, transform.fallback);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
225
252
|
ensurePreviewPath(node, transform.field);
|
|
226
253
|
ensureStyleAttrs(node, transform.field, inferButtonKind(transform.tag));
|
|
227
254
|
replaceTextChildren(node, transform.field, transform.fallback, transform.fieldType || 'text');
|
|
@@ -619,6 +646,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
619
646
|
|
|
620
647
|
const supported = new Set([
|
|
621
648
|
'split-action-contract',
|
|
649
|
+
'form-submit-action',
|
|
622
650
|
'extract-url',
|
|
623
651
|
'extract-image',
|
|
624
652
|
'extract-alt',
|
|
@@ -851,7 +879,7 @@ function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-u
|
|
|
851
879
|
ensureDefaultImport(ast, siteDataImport, jsonIdent);
|
|
852
880
|
}
|
|
853
881
|
|
|
854
|
-
const hasProvider = /SiteDataProvider|DenebDataProvider/.test(code);
|
|
882
|
+
const hasProvider = /SiteDataProvider|DenebDataProvider|<Providers\b/.test(code);
|
|
855
883
|
let wrapped = hasProvider;
|
|
856
884
|
|
|
857
885
|
if (!hasProvider) {
|