@deneb-ui/cli 2.0.47 → 2.0.49
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 +174 -0
- package/src/arc/adapters.cjs +74 -0
- package/src/arc/field-paths.cjs +2 -2
- package/src/arc/planner.cjs +4 -1
- package/src/arc/semantic.cjs +17 -8
- package/src/arc/transformer.cjs +35 -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.49",
|
|
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.49",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -15,6 +15,7 @@ const { enrichSchemasFromContent } = require('../manifest.cjs');
|
|
|
15
15
|
const { runDenebArc } = require('../index.cjs');
|
|
16
16
|
const { parseSource } = require('../ast.cjs');
|
|
17
17
|
const { loadFingerprintBoost } = require('../learning.cjs');
|
|
18
|
+
const { classifyActionIntent } = require('../adapters.cjs');
|
|
18
19
|
|
|
19
20
|
test('field-paths recognizes list action CTA keys', () => {
|
|
20
21
|
assert.equal(isListActionCtaKey('preOrderCta'), true);
|
|
@@ -701,3 +702,176 @@ export function BrandMarquee() {
|
|
|
701
702
|
assert.doesNotThrow(() => parseSource(result.code, 'BrandMarquee.tsx'));
|
|
702
703
|
});
|
|
703
704
|
|
|
705
|
+
test('classifyActionIntent identifies action keywords and assigns correct fallback URLs', () => {
|
|
706
|
+
const wa = classifyActionIntent('Order on WhatsApp', null);
|
|
707
|
+
assert.equal(wa?.action, 'whatsapp');
|
|
708
|
+
assert.equal(wa?.defaultUrl, 'https://wa.me/1234567890');
|
|
709
|
+
assert.equal(wa?.external, true);
|
|
710
|
+
|
|
711
|
+
const call = classifyActionIntent('Call Us', null);
|
|
712
|
+
assert.equal(call?.action, 'phone');
|
|
713
|
+
assert.equal(call?.defaultUrl, 'tel:+1234567890');
|
|
714
|
+
|
|
715
|
+
const dir = classifyActionIntent('Get Directions', null);
|
|
716
|
+
assert.equal(dir?.action, 'directions');
|
|
717
|
+
assert.equal(dir?.defaultUrl, 'https://maps.google.com/?q=store+location');
|
|
718
|
+
assert.equal(dir?.external, true);
|
|
719
|
+
|
|
720
|
+
const loc = classifyActionIntent('Our Location', null);
|
|
721
|
+
assert.equal(loc?.action, 'location');
|
|
722
|
+
assert.equal(loc?.defaultUrl, 'https://maps.google.com/?q=store+location');
|
|
723
|
+
assert.equal(loc?.external, true);
|
|
724
|
+
|
|
725
|
+
const shop = classifyActionIntent('Shop Now', null);
|
|
726
|
+
assert.equal(shop?.action, 'shop');
|
|
727
|
+
assert.equal(shop?.defaultUrl, '/shop');
|
|
728
|
+
assert.equal(shop?.external, false);
|
|
729
|
+
|
|
730
|
+
const nonAction = classifyActionIntent('Submit Form', null);
|
|
731
|
+
assert.equal(nonAction, null);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test('semantic engine recognizes <button> with action text as split-action-contract', () => {
|
|
735
|
+
const code = `
|
|
736
|
+
export function ContactBar() {
|
|
737
|
+
return (
|
|
738
|
+
<div className="contact-bar">
|
|
739
|
+
<button className="btn-primary">Order on WhatsApp</button>
|
|
740
|
+
<button className="btn-secondary">Get Directions</button>
|
|
741
|
+
<button type="submit">Submit Feedback</button>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
`;
|
|
746
|
+
const profile = {
|
|
747
|
+
root: os.tmpdir(),
|
|
748
|
+
framework: 'nextjs',
|
|
749
|
+
router: 'next-app',
|
|
750
|
+
language: 'typescript',
|
|
751
|
+
cssSystems: ['tailwind'],
|
|
752
|
+
hasSrc: true,
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const analysis = analyzeFile({
|
|
756
|
+
code,
|
|
757
|
+
relativeFile: 'src/components/ContactBar.tsx',
|
|
758
|
+
profile,
|
|
759
|
+
graph: { sharedFiles: [] },
|
|
760
|
+
ownerScope: 'home',
|
|
761
|
+
componentMeta: { name: 'ContactBar', role: 'contact' },
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const waSplit = analysis.candidates.find((c) => c.operation === 'split-action-contract' && c.label === 'Order on WhatsApp');
|
|
765
|
+
assert.ok(waSplit, 'expected split-action-contract for WhatsApp button');
|
|
766
|
+
assert.equal(waSplit.extra.action, 'whatsapp');
|
|
767
|
+
assert.equal(waSplit.value, 'https://wa.me/1234567890');
|
|
768
|
+
|
|
769
|
+
const dirSplit = analysis.candidates.find((c) => c.operation === 'split-action-contract' && c.label === 'Get Directions');
|
|
770
|
+
assert.ok(dirSplit, 'expected split-action-contract for Directions button');
|
|
771
|
+
assert.equal(dirSplit.extra.action, 'directions');
|
|
772
|
+
|
|
773
|
+
// Submit button should NOT be split into a redirect link
|
|
774
|
+
const submitSplit = analysis.candidates.find((c) => c.label === 'Submit Feedback' && c.operation === 'split-action-contract');
|
|
775
|
+
assert.equal(submitSplit, undefined, 'submit button must not be an action redirect');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test('AST transformer converts <button> action to <a> with redirect URL and editable text label', () => {
|
|
779
|
+
const code = `
|
|
780
|
+
export function ActionPanel() {
|
|
781
|
+
return (
|
|
782
|
+
<div className="panel">
|
|
783
|
+
<button className="px-4 py-2 bg-green-600 text-white rounded">Order on WhatsApp</button>
|
|
784
|
+
</div>
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
`;
|
|
788
|
+
const profile = {
|
|
789
|
+
root: os.tmpdir(),
|
|
790
|
+
framework: 'nextjs',
|
|
791
|
+
router: 'next-app',
|
|
792
|
+
language: 'typescript',
|
|
793
|
+
cssSystems: ['tailwind'],
|
|
794
|
+
hasSrc: true,
|
|
795
|
+
aliasMap: { '@/*': ['src/*'] },
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const analysis = analyzeFile({
|
|
799
|
+
code,
|
|
800
|
+
relativeFile: 'src/components/ActionPanel.tsx',
|
|
801
|
+
profile,
|
|
802
|
+
graph: { sharedFiles: [] },
|
|
803
|
+
ownerScope: 'home',
|
|
804
|
+
componentMeta: { name: 'ActionPanel', role: 'hero' },
|
|
805
|
+
});
|
|
806
|
+
analysis.code = code;
|
|
807
|
+
analysis.relativeFile = 'src/components/ActionPanel.tsx';
|
|
808
|
+
|
|
809
|
+
const plan = planTransformations({ profile, analyses: [analysis] });
|
|
810
|
+
const result = applyFilePlan(plan.files[0], profile);
|
|
811
|
+
|
|
812
|
+
assert.equal(result.changed, true);
|
|
813
|
+
// Converted from <button> to <a ...>
|
|
814
|
+
assert.match(result.code, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.hero\?\.whatsappUrl \?\? "https:\/\/wa\.me\/1234567890"\}/);
|
|
815
|
+
assert.match(result.code, /target="_blank"/);
|
|
816
|
+
assert.match(result.code, /rel="noopener noreferrer"/);
|
|
817
|
+
assert.match(result.code, /data-preview-static="action-link"/);
|
|
818
|
+
// Text label wrapped in editable span
|
|
819
|
+
assert.match(result.code, /<span\s+data-preview-field-path="home\.hero\.whatsappLabel"[^>]*>\{siteData\?\.content\?\.home\?\.hero\?\.whatsappLabel \?\? "Order on WhatsApp"\}<\/span>/);
|
|
820
|
+
// Hidden URL span present for Fivora contract
|
|
821
|
+
assert.match(result.code, /<span hidden aria-hidden="true" data-preview-field-path="home\.hero\.whatsappUrl">/);
|
|
822
|
+
// No parse errors
|
|
823
|
+
assert.doesNotThrow(() => parseSource(result.code, 'ActionPanel.tsx'));
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
test('end-to-end ARC conversion transforms action buttons and passes strict Fivora audit', () => {
|
|
827
|
+
const dir = copyOf(FIXTURE);
|
|
828
|
+
const actionPagePath = path.join(dir, 'src', 'components', 'ActionPage.tsx');
|
|
829
|
+
fs.writeFileSync(
|
|
830
|
+
actionPagePath,
|
|
831
|
+
`
|
|
832
|
+
export function ActionPage() {
|
|
833
|
+
return (
|
|
834
|
+
<div className="action-page p-8">
|
|
835
|
+
<h1>Connect & Visit</h1>
|
|
836
|
+
<button className="btn-wa">Order on WhatsApp</button>
|
|
837
|
+
<button className="btn-call">Call Us</button>
|
|
838
|
+
<button className="btn-dir">Get Directions</button>
|
|
839
|
+
<button className="btn-loc">Our Location</button>
|
|
840
|
+
<button className="btn-shop">Shop Now</button>
|
|
841
|
+
</div>
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
`
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
// Link ActionPage in page.tsx
|
|
848
|
+
const pageFile = path.join(dir, 'src', 'app', 'page.tsx');
|
|
849
|
+
const pageSrc = fs.readFileSync(pageFile, 'utf8');
|
|
850
|
+
fs.writeFileSync(
|
|
851
|
+
pageFile,
|
|
852
|
+
`import { ActionPage } from '../components/ActionPage';\n` +
|
|
853
|
+
pageSrc.replace('</main>', ' <ActionPage />\n </main>')
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
silence(() => runDenebArc(dir, 'actions-fixture', { telemetry: 'off' }));
|
|
857
|
+
|
|
858
|
+
const transformed = fs.readFileSync(actionPagePath, 'utf8');
|
|
859
|
+
// All 5 buttons converted to <a> tags with href
|
|
860
|
+
assert.match(transformed, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.contact\?\.whatsappUrl/);
|
|
861
|
+
assert.match(transformed, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.contact\?\.phoneUrl/);
|
|
862
|
+
assert.match(transformed, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.contact\?\.getDirectionsUrl/);
|
|
863
|
+
assert.match(transformed, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.contact\?\.ourLocationUrl/);
|
|
864
|
+
assert.match(transformed, /<a\s+[^>]*href=\{siteData\?\.content\?\.home\?\.shop\?\.shopNowUrl/);
|
|
865
|
+
|
|
866
|
+
// All 5 buttons have editable labels
|
|
867
|
+
assert.match(transformed, /data-preview-field-path="home\.contact\.whatsappLabel"/);
|
|
868
|
+
assert.match(transformed, /data-preview-field-path="home\.contact\.phoneLabel"/);
|
|
869
|
+
assert.match(transformed, /data-preview-field-path="home\.contact\.getDirectionsLabel"/);
|
|
870
|
+
assert.match(transformed, /data-preview-field-path="home\.contact\.ourLocationLabel"/);
|
|
871
|
+
assert.match(transformed, /data-preview-field-path="home\.shop\.shopNowLabel"/);
|
|
872
|
+
|
|
873
|
+
// Strict Fivora audit passes with 0 errors
|
|
874
|
+
const { errors } = auditFivora(dir);
|
|
875
|
+
assert.deepEqual(errors, []);
|
|
876
|
+
});
|
|
877
|
+
|
package/src/arc/adapters.cjs
CHANGED
|
@@ -188,6 +188,79 @@ function isLikelyCtaClass(className) {
|
|
|
188
188
|
return /\b(btn|button|cta|action|rounded|bg-|hero[-_]?cta)\b/i.test(className || '');
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
function classifyActionIntent(text, href) {
|
|
192
|
+
const t = String(text || '').trim().toLowerCase();
|
|
193
|
+
const h = String(href || '').trim().toLowerCase();
|
|
194
|
+
|
|
195
|
+
// 1. WhatsApp
|
|
196
|
+
if (/wa\.me|whatsapp/i.test(h) || /\b(?:whatsapp|wa\.me)\b/i.test(t) || /order on whatsapp|chat on whatsapp|message on whatsapp/i.test(t)) {
|
|
197
|
+
return {
|
|
198
|
+
action: 'whatsapp',
|
|
199
|
+
defaultUrl: /^https?:\/\//i.test(h) && !h.includes('#') ? href : 'https://wa.me/1234567890',
|
|
200
|
+
external: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2. Phone / Call
|
|
205
|
+
if (/^tel:/i.test(h) || /\b(?:call\s*(?:me|us|now)?|phone\s*(?:me|us|now)?|direct\s*line|ring\s*us)\b/i.test(t)) {
|
|
206
|
+
return {
|
|
207
|
+
action: 'phone',
|
|
208
|
+
defaultUrl: /^tel:/i.test(h) ? href : 'tel:+1234567890',
|
|
209
|
+
external: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 3. Directions
|
|
214
|
+
if (/maps\.google|goo\.gl\/maps|map\.apple/i.test(h) || /\b(?:directions?|get\s*directions?|route|navigate)\b/i.test(t)) {
|
|
215
|
+
return {
|
|
216
|
+
action: 'directions',
|
|
217
|
+
defaultUrl: /^https?:\/\//i.test(h) && !h.includes('#') ? href : 'https://maps.google.com/?q=store+location',
|
|
218
|
+
external: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 4. Location / Map
|
|
223
|
+
if (/\b(?:locations?|our\s*location|store\s*location|view\s*location|find\s*us|visit\s*us|locate\s*us|map)\b/i.test(t)) {
|
|
224
|
+
return {
|
|
225
|
+
action: 'location',
|
|
226
|
+
defaultUrl: /^https?:\/\//i.test(h) && !h.includes('#') ? href : 'https://maps.google.com/?q=store+location',
|
|
227
|
+
external: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 5. Shop / Order / Catalog / Menu
|
|
232
|
+
if (/\b(?:shop(?:\s*now)?|order(?:\s*now)?|buy(?:\s*now)?|explore\s*(?:shop|products|offerings|collection|menu)|browse\s*(?:menu|catalog|shop)|view\s*(?:menu|catalog)|menu)\b/i.test(t) || /^\/(?:shop|products|menu|catalog)/i.test(h)) {
|
|
233
|
+
return {
|
|
234
|
+
action: 'shop',
|
|
235
|
+
defaultUrl: h && !['#', ''].includes(h) ? href : '/shop',
|
|
236
|
+
external: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 6. Email
|
|
241
|
+
if (/^mailto:/i.test(h) || /\b(?:email\s*us|send\s*(?:us\s*)?email|contact\s*by\s*email)\b/i.test(t)) {
|
|
242
|
+
return {
|
|
243
|
+
action: 'email',
|
|
244
|
+
defaultUrl: /^mailto:/i.test(h) ? href : 'mailto:info@example.com',
|
|
245
|
+
external: false,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 7. Check href using existing classifyHref
|
|
250
|
+
if (h && !['#', ''].includes(h)) {
|
|
251
|
+
const fromHref = classifyHref(href);
|
|
252
|
+
if (fromHref && fromHref !== 'link') {
|
|
253
|
+
return {
|
|
254
|
+
action: fromHref,
|
|
255
|
+
defaultUrl: href,
|
|
256
|
+
external: ['instagram', 'facebook', 'tiktok', 'twitter', 'youtube', 'linkedin', 'pinterest'].includes(fromHref),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
191
264
|
module.exports = {
|
|
192
265
|
ADAPTERS,
|
|
193
266
|
activeAdapters,
|
|
@@ -195,6 +268,7 @@ module.exports = {
|
|
|
195
268
|
resolveActionWithAdapters,
|
|
196
269
|
isIconComponent,
|
|
197
270
|
classifyHref,
|
|
271
|
+
classifyActionIntent,
|
|
198
272
|
isLikelyCtaClass,
|
|
199
273
|
DECORATIVE_TAGS,
|
|
200
274
|
SKIP_TAGS,
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -71,14 +71,14 @@ function inferFieldName(kind, tag, text, extra = {}) {
|
|
|
71
71
|
if (extra.action === 'email') return 'emailUrl';
|
|
72
72
|
if (extra.cta) return extra.cta === 'primary' ? 'primaryCtaUrl' : `${extra.cta}Url`;
|
|
73
73
|
const fromText = toCamel([text || '', 'url']);
|
|
74
|
-
return fromText || 'ctaUrl';
|
|
74
|
+
return fromText || (extra.action ? `${extra.action}Url` : 'ctaUrl');
|
|
75
75
|
}
|
|
76
76
|
if (kind === 'label' && extra.paired) {
|
|
77
77
|
if (extra.action === 'whatsapp') return 'whatsappLabel';
|
|
78
78
|
if (extra.action === 'phone') return 'phoneLabel';
|
|
79
79
|
if (extra.action === 'email') return 'emailLabel';
|
|
80
80
|
if (extra.cta) return extra.cta === 'primary' ? 'primaryCtaLabel' : `${extra.cta}Label`;
|
|
81
|
-
return toCamel([text || '', 'label']) || 'ctaLabel';
|
|
81
|
+
return toCamel([text || '', 'label']) || (extra.action ? `${extra.action}Label` : 'ctaLabel');
|
|
82
82
|
}
|
|
83
83
|
if (kind === 'image') return extra.alt ? toCamel([extra.alt, 'image']) || 'image' : 'image';
|
|
84
84
|
if (kind === 'alt') return extra.imageField ? extra.imageField.replace(/Image$/, 'ImageAlt').replace(/image$/, 'imageAlt') : 'imageAlt';
|
package/src/arc/planner.cjs
CHANGED
|
@@ -230,9 +230,12 @@ function sectionForAction(scope, section, extra) {
|
|
|
230
230
|
if (scope === 'common' && (extra.social || ['instagram', 'facebook', 'twitter', 'tiktok', 'youtube', 'linkedin'].includes(extra.action))) {
|
|
231
231
|
return 'footer';
|
|
232
232
|
}
|
|
233
|
-
if (
|
|
233
|
+
if (['whatsapp', 'phone', 'email', 'directions', 'location'].includes(extra.action)) {
|
|
234
234
|
return section || 'contact';
|
|
235
235
|
}
|
|
236
|
+
if (extra.action === 'shop') {
|
|
237
|
+
return section || 'shop';
|
|
238
|
+
}
|
|
236
239
|
return section;
|
|
237
240
|
}
|
|
238
241
|
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
resolveActionWithAdapters,
|
|
17
17
|
isIconComponent,
|
|
18
18
|
classifyHref,
|
|
19
|
+
classifyActionIntent,
|
|
19
20
|
isLikelyCtaClass,
|
|
20
21
|
SKIP_TAGS,
|
|
21
22
|
HEADING_TAGS,
|
|
@@ -474,22 +475,30 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
474
475
|
}
|
|
475
476
|
|
|
476
477
|
const actionableHref = href || (name === 'Button' ? getJsxAttributeLiteral(node, 'href') : null);
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
478
|
+
const innerText = textInfo.dynamic ? '' : textInfo.text;
|
|
479
|
+
const typeAttr = getJsxAttributeLiteral(node, 'type');
|
|
480
|
+
const isSubmit = typeAttr === 'submit';
|
|
481
|
+
|
|
482
|
+
// Check for action intent via classifyActionIntent (covers WhatsApp, Call/Phone, Directions, Location, Shop, Email)
|
|
483
|
+
const actionIntent = !isSubmit && !apiOwned ? classifyActionIntent(innerText, actionableHref) : null;
|
|
484
|
+
const isAction = (Boolean(actionableHref) || Boolean(actionIntent)) && (ACTION_TAGS.has(name) || isLikelyCtaClass(className));
|
|
485
|
+
|
|
486
|
+
if (isAction && (actionableHref || actionIntent)) {
|
|
487
|
+
const action = actionIntent ? actionIntent.action : classifyHref(actionableHref);
|
|
488
|
+
const resolvedHref = actionableHref || (actionIntent ? actionIntent.defaultUrl : '#');
|
|
489
|
+
const looksCta = isLikelyCtaClass(className) || ['whatsapp', 'phone', 'email', 'directions', 'location', 'shop'].includes(action) || Boolean(innerText);
|
|
482
490
|
if (looksCta && innerText && !textInfo.dynamic && !apiOwned) {
|
|
483
491
|
usedLocs.add(loc);
|
|
484
492
|
candidates.push({
|
|
485
493
|
...baseMeta,
|
|
486
494
|
kind: 'split-action-contract',
|
|
487
495
|
operation: 'split-action-contract',
|
|
488
|
-
value:
|
|
496
|
+
value: resolvedHref,
|
|
489
497
|
label: innerText,
|
|
490
498
|
extra: {
|
|
491
499
|
action,
|
|
492
|
-
|
|
500
|
+
external: actionIntent ? actionIntent.external : false,
|
|
501
|
+
social: !['whatsapp', 'phone', 'email', 'directions', 'location', 'shop', 'link'].includes(action),
|
|
493
502
|
platform: ['instagram', 'facebook', 'tiktok', 'twitter', 'youtube', 'linkedin', 'pinterest'].includes(action) ? action : undefined,
|
|
494
503
|
},
|
|
495
504
|
confidence: confidenceFor('split-action-contract', { action, social: action !== 'link' }),
|
|
@@ -500,7 +509,7 @@ function analyzeFile({ code, relativeFile, profile, graph, ownerScope, component
|
|
|
500
509
|
return;
|
|
501
510
|
}
|
|
502
511
|
|
|
503
|
-
if (action !== 'link' && !innerText) {
|
|
512
|
+
if (action !== 'link' && !innerText && actionableHref) {
|
|
504
513
|
usedLocs.add(loc);
|
|
505
514
|
candidates.push({
|
|
506
515
|
...baseMeta,
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -142,14 +142,48 @@ function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
|
|
|
142
142
|
[b.jsxExpressionContainer(siteDataBinding(urlParts, fallback, 'url'))],
|
|
143
143
|
false
|
|
144
144
|
);
|
|
145
|
-
pathNode.
|
|
145
|
+
if (pathNode.parent && Array.isArray(pathNode.parent.node?.children)) {
|
|
146
|
+
pathNode.insertAfter(hiddenUrl);
|
|
147
|
+
} else if (pathNode.node && Array.isArray(pathNode.node.children)) {
|
|
148
|
+
pathNode.node.children = [hiddenUrl, ...(pathNode.node.children || [])];
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
function applyTransformToElement(pathNode, transform) {
|
|
149
153
|
const node = pathNode.node;
|
|
150
154
|
if (transform.operation === 'split-action-contract') {
|
|
155
|
+
const tagName = getJsxName(node);
|
|
156
|
+
if (tagName === 'button') {
|
|
157
|
+
node.openingElement.name = b.jsxIdentifier('a');
|
|
158
|
+
if (node.closingElement) {
|
|
159
|
+
node.closingElement.name = b.jsxIdentifier('a');
|
|
160
|
+
}
|
|
161
|
+
node.openingElement.attributes = (node.openingElement.attributes || []).filter(
|
|
162
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'type')
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
const urlParts = transform.urlField.split('.');
|
|
152
167
|
replaceAttrValue(node, 'href', siteDataBinding(urlParts, transform.fallback, 'url'));
|
|
168
|
+
|
|
169
|
+
const isExternal =
|
|
170
|
+
transform.extra?.external ||
|
|
171
|
+
['whatsapp', 'directions', 'location'].includes(transform.extra?.action) ||
|
|
172
|
+
/^https?:\/\//i.test(String(transform.fallback || ''));
|
|
173
|
+
|
|
174
|
+
if (isExternal && (tagName === 'button' || tagName === 'a')) {
|
|
175
|
+
if (!hasJsxAttribute(node, 'target')) {
|
|
176
|
+
node.openingElement.attributes.push(
|
|
177
|
+
b.jsxAttribute(b.jsxIdentifier('target'), b.stringLiteral('_blank'))
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (!hasJsxAttribute(node, 'rel')) {
|
|
181
|
+
node.openingElement.attributes.push(
|
|
182
|
+
b.jsxAttribute(b.jsxIdentifier('rel'), b.stringLiteral('noopener noreferrer'))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
if (!hasJsxAttribute(node, 'data-preview-static')) {
|
|
154
188
|
node.openingElement.attributes.push(
|
|
155
189
|
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-link'))
|