@deneb-ui/cli 2.0.21 → 2.0.23
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/README.md +62 -114
- package/bin/index.js +101 -207
- package/package.json +20 -5
- package/src/arc/__fixtures__/next-app-basic/package.json +10 -0
- package/src/arc/__fixtures__/next-app-basic/src/app/globals.css +3 -0
- package/src/arc/__fixtures__/next-app-basic/src/app/layout.tsx +9 -0
- package/src/arc/__fixtures__/next-app-basic/src/app/page.tsx +11 -0
- package/src/arc/__fixtures__/next-app-basic/src/components/Header.tsx +13 -0
- package/src/arc/__fixtures__/next-app-basic/src/components/Hero.tsx +12 -0
- package/src/arc/__fixtures__/next-app-basic/src/components/PromoBanner.tsx +10 -0
- package/src/arc/__fixtures__/next-app-basic/tsconfig.json +12 -0
- package/src/arc/__fixtures__/next-app-storefront/components.json +14 -0
- package/src/arc/__fixtures__/next-app-storefront/package.json +22 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/about/page.tsx +13 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/globals.css +5 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/layout.tsx +19 -0
- package/src/arc/__fixtures__/next-app-storefront/src/app/page.tsx +13 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/Features.tsx +31 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/Hero.tsx +45 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/ProductGrid.tsx +49 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/SiteFooter.tsx +22 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/SiteHeader.tsx +21 -0
- package/src/arc/__fixtures__/next-app-storefront/src/components/ui/button.tsx +36 -0
- package/src/arc/__fixtures__/next-app-storefront/tsconfig.json +15 -0
- package/src/arc/__fixtures__/next-pages-basic/package.json +10 -0
- package/src/arc/__fixtures__/next-pages-basic/pages/_app.jsx +5 -0
- package/src/arc/__fixtures__/next-pages-basic/pages/contact.jsx +9 -0
- package/src/arc/__fixtures__/next-pages-basic/pages/index.jsx +11 -0
- package/src/arc/__fixtures__/next-pages-basic/styles/globals.css +9 -0
- package/src/arc/__tests__/arc.test.cjs +458 -0
- package/src/arc/adapters.cjs +184 -0
- package/src/arc/ast.cjs +323 -0
- package/src/arc/field-paths.cjs +165 -0
- package/src/arc/fivora-contract.cjs +521 -0
- package/src/arc/fs-utils.cjs +170 -0
- package/src/arc/index.cjs +628 -0
- package/src/arc/learning.cjs +185 -0
- package/src/arc/manifest.cjs +421 -0
- package/src/arc/next-config.cjs +279 -0
- package/src/arc/planner.cjs +227 -0
- package/src/arc/printer.cjs +153 -0
- package/src/arc/recipes-v2.cjs +49 -0
- package/src/arc/scanner.cjs +613 -0
- package/src/arc/semantic.cjs +651 -0
- package/src/arc/transformer.cjs +646 -0
- package/src/arc/validator.cjs +173 -0
- package/src/arc/version.cjs +22 -0
- package/src/recipes/cosmetics-beauty-store.json +1097 -0
- package/src/recipes/electronics-gadgets-store.json +1080 -0
- package/src/recipes/fashion-apparel-store.json +1074 -0
- package/src/tools/deneb-doctor.cjs +646 -0
- package/src/tools/recipe-engine.cjs +30 -0
- package/src/tools/template-converter.cjs +19 -6
package/src/arc/ast.cjs
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const recast = require('recast');
|
|
4
|
+
const babelParser = require('@babel/parser');
|
|
5
|
+
const t = recast.types.namedTypes;
|
|
6
|
+
const b = recast.types.builders;
|
|
7
|
+
|
|
8
|
+
let babelTsParser = null;
|
|
9
|
+
try {
|
|
10
|
+
babelTsParser = require('recast/parsers/babel-ts');
|
|
11
|
+
} catch {
|
|
12
|
+
babelTsParser = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BABEL_PLUGIN_SETS = [
|
|
16
|
+
['jsx', 'typescript', 'decorators-legacy', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'importAttributes'],
|
|
17
|
+
['jsx', 'typescript', 'decorators-legacy', 'classProperties', 'importAssertions'],
|
|
18
|
+
['jsx', 'typescript', 'classProperties'],
|
|
19
|
+
['jsx', 'typescript'],
|
|
20
|
+
['jsx'],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function parseWithBabel(code) {
|
|
24
|
+
let lastError = null;
|
|
25
|
+
for (const plugins of BABEL_PLUGIN_SETS) {
|
|
26
|
+
try {
|
|
27
|
+
return babelParser.parse(code, {
|
|
28
|
+
sourceType: 'unambiguous',
|
|
29
|
+
allowReturnOutsideFunction: true,
|
|
30
|
+
allowAwaitOutsideFunction: true,
|
|
31
|
+
errorRecovery: true,
|
|
32
|
+
tokens: true,
|
|
33
|
+
plugins,
|
|
34
|
+
});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
lastError = err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw lastError || new Error('Unable to parse source');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseSource(code, filePath = 'file.tsx') {
|
|
43
|
+
const options = { sourceFileName: filePath };
|
|
44
|
+
if (babelTsParser) {
|
|
45
|
+
try {
|
|
46
|
+
return recast.parse(code, { ...options, parser: babelTsParser });
|
|
47
|
+
} catch {
|
|
48
|
+
// Fall through to token-aware Babel parse.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return recast.parse(code, {
|
|
52
|
+
...options,
|
|
53
|
+
parser: {
|
|
54
|
+
parse(source) {
|
|
55
|
+
return parseWithBabel(source);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function printSource(ast, originalCode) {
|
|
62
|
+
let printed = recast.print(ast, {
|
|
63
|
+
quote: 'double',
|
|
64
|
+
wrapColumn: 120,
|
|
65
|
+
reuseWhitespace: true,
|
|
66
|
+
}).code;
|
|
67
|
+
printed = printed.replace(/(['"]use client['"]);;/g, '$1;');
|
|
68
|
+
printed = printed.replace(/(['"]use server['"]);;/g, '$1;');
|
|
69
|
+
if (typeof originalCode === 'string' && originalCode.endsWith('\n') && !printed.endsWith('\n')) {
|
|
70
|
+
printed += '\n';
|
|
71
|
+
}
|
|
72
|
+
return printed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function locKey(node) {
|
|
76
|
+
const loc = node && node.loc;
|
|
77
|
+
if (!loc || !loc.start) return null;
|
|
78
|
+
return `${loc.start.line}:${loc.start.column}:${loc.end ? loc.end.line : ''}:${loc.end ? loc.end.column : ''}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getJsxName(node) {
|
|
82
|
+
if (!node) return '';
|
|
83
|
+
const opening = node.openingElement || node;
|
|
84
|
+
const name = opening.name || node.name;
|
|
85
|
+
return jsxNameToString(name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function jsxNameToString(name) {
|
|
89
|
+
if (!name) return '';
|
|
90
|
+
if (name.type === 'JSXIdentifier') return name.name;
|
|
91
|
+
if (name.type === 'JSXMemberExpression') {
|
|
92
|
+
return `${jsxNameToString(name.object)}.${jsxNameToString(name.property)}`;
|
|
93
|
+
}
|
|
94
|
+
if (name.type === 'JSXNamespacedName') {
|
|
95
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
96
|
+
}
|
|
97
|
+
if (name.type === 'Identifier') return name.name;
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getJsxAttributes(node) {
|
|
102
|
+
const opening = node.openingElement || node;
|
|
103
|
+
return opening.attributes || [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function findJsxAttribute(node, attrName) {
|
|
107
|
+
return getJsxAttributes(node).find((attr) => {
|
|
108
|
+
return attr && attr.type === 'JSXAttribute' && attr.name && attr.name.name === attrName;
|
|
109
|
+
}) || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getJsxAttributeLiteral(node, attrName) {
|
|
113
|
+
const attr = findJsxAttribute(node, attrName);
|
|
114
|
+
if (!attr || !attr.value) return null;
|
|
115
|
+
if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
|
|
116
|
+
return String(attr.value.value);
|
|
117
|
+
}
|
|
118
|
+
if (attr.value.type === 'JSXExpressionContainer') {
|
|
119
|
+
const expr = attr.value.expression;
|
|
120
|
+
if (!expr) return null;
|
|
121
|
+
if (expr.type === 'StringLiteral' || expr.type === 'Literal') return String(expr.value);
|
|
122
|
+
if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
|
|
123
|
+
return expr.quasis.map((q) => q.value.cooked || q.value.raw || '').join('');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function hasJsxAttribute(node, attrName) {
|
|
130
|
+
return Boolean(findJsxAttribute(node, attrName));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collectJsxText(node) {
|
|
134
|
+
if (!node || !Array.isArray(node.children)) return '';
|
|
135
|
+
const parts = [];
|
|
136
|
+
for (const child of node.children) {
|
|
137
|
+
if (!child) continue;
|
|
138
|
+
if (child.type === 'JSXText') {
|
|
139
|
+
parts.push(child.value);
|
|
140
|
+
} else if (child.type === 'JSXExpressionContainer' && child.expression && (child.expression.type === 'StringLiteral' || child.expression.type === 'Literal')) {
|
|
141
|
+
parts.push(String(child.expression.value));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return parts.join('').replace(/\s+/g, ' ').trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isJsxTextHeavy(node) {
|
|
148
|
+
if (!node || !Array.isArray(node.children)) return false;
|
|
149
|
+
const meaningful = node.children.filter((child) => {
|
|
150
|
+
if (!child) return false;
|
|
151
|
+
if (child.type === 'JSXText') return child.value.replace(/\s+/g, '').length > 0;
|
|
152
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
153
|
+
const expr = child.expression;
|
|
154
|
+
return expr && (expr.type === 'StringLiteral' || expr.type === 'Literal' || expr.type === 'Identifier');
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
});
|
|
158
|
+
return meaningful.length > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function optionalMember(parts) {
|
|
162
|
+
let expr = b.identifier(parts[0]);
|
|
163
|
+
for (let i = 1; i < parts.length; i++) {
|
|
164
|
+
expr = b.optionalMemberExpression(expr, b.identifier(parts[i]), false, true);
|
|
165
|
+
}
|
|
166
|
+
return expr;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function siteDataBinding(pathParts, fallback, fieldType = 'string') {
|
|
170
|
+
const chain = optionalMember(['siteData', 'content', ...pathParts]);
|
|
171
|
+
let fallbackNode;
|
|
172
|
+
if (fieldType === 'number' && fallback !== '' && !Number.isNaN(Number(fallback))) {
|
|
173
|
+
fallbackNode = b.numericLiteral(Number(fallback));
|
|
174
|
+
} else if (fieldType === 'boolean') {
|
|
175
|
+
fallbackNode = b.booleanLiteral(Boolean(fallback));
|
|
176
|
+
} else {
|
|
177
|
+
fallbackNode = b.stringLiteral(String(fallback ?? ''));
|
|
178
|
+
}
|
|
179
|
+
return b.logicalExpression('??', chain, fallbackNode);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function jsxPreviewAttr(fieldPath) {
|
|
183
|
+
return b.jsxAttribute(
|
|
184
|
+
b.jsxIdentifier('data-preview-field-path'),
|
|
185
|
+
b.stringLiteral(fieldPath)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function jsxStaticAttr(reason) {
|
|
190
|
+
return b.jsxAttribute(
|
|
191
|
+
b.jsxIdentifier('data-preview-static'),
|
|
192
|
+
b.stringLiteral(reason || 'decorative')
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Binds a whole collection to site data while keeping the developer's literal
|
|
198
|
+
* array as the fallback, so `{item.title}` inside the existing map picks up
|
|
199
|
+
* merchant edits with no change to the render logic.
|
|
200
|
+
*/
|
|
201
|
+
function siteDataListBinding(pathParts, fallbackArrayNode) {
|
|
202
|
+
return b.logicalExpression(
|
|
203
|
+
'??',
|
|
204
|
+
optionalMember(['siteData', 'content', ...pathParts]),
|
|
205
|
+
fallbackArrayNode
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Builds `attr={`prefix[${index}]suffix`}`. Fivora's marker parser accepts JSX
|
|
211
|
+
* template literals, which is how repeated items stay editable after reorder.
|
|
212
|
+
*/
|
|
213
|
+
function jsxTemplatePathAttr(attrName, prefix, indexName, suffix = '') {
|
|
214
|
+
return b.jsxAttribute(
|
|
215
|
+
b.jsxIdentifier(attrName),
|
|
216
|
+
b.jsxExpressionContainer(
|
|
217
|
+
b.templateLiteral(
|
|
218
|
+
[
|
|
219
|
+
b.templateElement({ raw: `${prefix}[`, cooked: `${prefix}[` }, false),
|
|
220
|
+
b.templateElement({ raw: `]${suffix}`, cooked: `]${suffix}` }, true),
|
|
221
|
+
],
|
|
222
|
+
[b.identifier(indexName)]
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function wrapTextInEditableSpan(fieldPath, fallback, fieldType) {
|
|
229
|
+
return b.jsxElement(
|
|
230
|
+
b.jsxOpeningElement(
|
|
231
|
+
b.jsxIdentifier('span'),
|
|
232
|
+
[jsxPreviewAttr(fieldPath)],
|
|
233
|
+
false
|
|
234
|
+
),
|
|
235
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
236
|
+
[b.jsxExpressionContainer(siteDataBinding(fieldPath.split('.'), fallback, fieldType))],
|
|
237
|
+
false
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function hasDirective(ast, value) {
|
|
242
|
+
const program = ast.program || ast;
|
|
243
|
+
const first = program.body && program.body[0];
|
|
244
|
+
if (first && first.type === 'ExpressionStatement' && first.expression) {
|
|
245
|
+
const expr = first.expression;
|
|
246
|
+
if ((expr.type === 'StringLiteral' || expr.type === 'Literal') && expr.value === value) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function ensureImport(ast, source, names) {
|
|
254
|
+
const program = ast.program || ast;
|
|
255
|
+
const body = program.body || [];
|
|
256
|
+
const existing = body.find((node) => node.type === 'ImportDeclaration' && node.source && node.source.value === source);
|
|
257
|
+
if (existing) {
|
|
258
|
+
const already = new Set(
|
|
259
|
+
(existing.specifiers || [])
|
|
260
|
+
.filter((s) => s.type === 'ImportSpecifier')
|
|
261
|
+
.map((s) => s.imported?.name || s.local?.name)
|
|
262
|
+
);
|
|
263
|
+
for (const name of names) {
|
|
264
|
+
if (!already.has(name)) {
|
|
265
|
+
existing.specifiers.push(b.importSpecifier(b.identifier(name), b.identifier(name)));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const specifiers = names.map((name) => b.importSpecifier(b.identifier(name), b.identifier(name)));
|
|
272
|
+
const decl = b.importDeclaration(specifiers, b.stringLiteral(source));
|
|
273
|
+
let insertAt = 0;
|
|
274
|
+
if (body[0] && body[0].type === 'ExpressionStatement') insertAt = 1;
|
|
275
|
+
body.splice(insertAt, 0, decl);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function ensureDefaultImport(ast, source, localName) {
|
|
279
|
+
const program = ast.program || ast;
|
|
280
|
+
const body = program.body || [];
|
|
281
|
+
const existing = body.find((node) => node.type === 'ImportDeclaration' && node.source && node.source.value === source);
|
|
282
|
+
if (existing) return;
|
|
283
|
+
const decl = b.importDeclaration(
|
|
284
|
+
[b.importDefaultSpecifier(b.identifier(localName))],
|
|
285
|
+
b.stringLiteral(source)
|
|
286
|
+
);
|
|
287
|
+
let insertAt = 0;
|
|
288
|
+
if (body[0] && body[0].type === 'ExpressionStatement') insertAt = 1;
|
|
289
|
+
body.splice(insertAt, 0, decl);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function codeHasIdentifier(code, name) {
|
|
293
|
+
return new RegExp(`\\b${name}\\b`).test(code);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
parseSource,
|
|
298
|
+
printSource,
|
|
299
|
+
parseWithBabel,
|
|
300
|
+
locKey,
|
|
301
|
+
getJsxName,
|
|
302
|
+
jsxNameToString,
|
|
303
|
+
getJsxAttributes,
|
|
304
|
+
findJsxAttribute,
|
|
305
|
+
getJsxAttributeLiteral,
|
|
306
|
+
hasJsxAttribute,
|
|
307
|
+
collectJsxText,
|
|
308
|
+
isJsxTextHeavy,
|
|
309
|
+
optionalMember,
|
|
310
|
+
siteDataBinding,
|
|
311
|
+
siteDataListBinding,
|
|
312
|
+
jsxPreviewAttr,
|
|
313
|
+
jsxStaticAttr,
|
|
314
|
+
jsxTemplatePathAttr,
|
|
315
|
+
wrapTextInEditableSpan,
|
|
316
|
+
hasDirective,
|
|
317
|
+
ensureImport,
|
|
318
|
+
ensureDefaultImport,
|
|
319
|
+
codeHasIdentifier,
|
|
320
|
+
t,
|
|
321
|
+
b,
|
|
322
|
+
visit: recast.types.visit,
|
|
323
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STOP_WORDS = new Set([
|
|
4
|
+
'the', 'and', 'for', 'with', 'from', 'this', 'that', 'your', 'our', 'are', 'you',
|
|
5
|
+
'a', 'an', 'to', 'of', 'in', 'on', 'at', 'by', 'or', 'is',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function toCamel(parts) {
|
|
9
|
+
const cleaned = parts
|
|
10
|
+
.join(' ')
|
|
11
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
12
|
+
.trim()
|
|
13
|
+
.split(/\s+/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.filter((w, i) => i === 0 || !STOP_WORDS.has(w.toLowerCase()))
|
|
16
|
+
.slice(0, 5);
|
|
17
|
+
if (!cleaned.length) return '';
|
|
18
|
+
return cleaned
|
|
19
|
+
.map((word, i) => {
|
|
20
|
+
const lower = word.toLowerCase();
|
|
21
|
+
if (i === 0) return lower;
|
|
22
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
23
|
+
})
|
|
24
|
+
.join('');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function inferSection(context) {
|
|
28
|
+
const haystack = [
|
|
29
|
+
context.componentName,
|
|
30
|
+
context.fileName,
|
|
31
|
+
context.className,
|
|
32
|
+
context.parentName,
|
|
33
|
+
context.tag,
|
|
34
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
35
|
+
|
|
36
|
+
const rules = [
|
|
37
|
+
['announcement', /announc|promo-bar|topbar/],
|
|
38
|
+
['header', /header|navbar|nav\b|site-header/],
|
|
39
|
+
['footer', /footer|site-footer/],
|
|
40
|
+
['hero', /hero|banner|jumbotron/],
|
|
41
|
+
['navigation', /nav|menu|links/],
|
|
42
|
+
['testimonials', /testimonial|review/],
|
|
43
|
+
['faq', /faq|accordion/],
|
|
44
|
+
['contact', /contact|whatsapp|mailto/],
|
|
45
|
+
['featuredProducts', /featured|product-grid|collection/],
|
|
46
|
+
['newsletter', /newsletter|subscribe/],
|
|
47
|
+
['pricing', /pricing|plan/],
|
|
48
|
+
['features', /feature/],
|
|
49
|
+
['about', /about|story|brand/],
|
|
50
|
+
];
|
|
51
|
+
for (const [name, re] of rules) {
|
|
52
|
+
if (re.test(haystack)) return name;
|
|
53
|
+
}
|
|
54
|
+
if (context.role === 'navigation') return 'header';
|
|
55
|
+
if (context.role === 'footer') return 'footer';
|
|
56
|
+
if (context.role === 'hero') return 'hero';
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inferFieldName(kind, tag, text, extra = {}) {
|
|
61
|
+
if (kind === 'url') {
|
|
62
|
+
if (extra.platform) return `${extra.platform}Url`;
|
|
63
|
+
if (extra.action === 'whatsapp') return 'whatsappUrl';
|
|
64
|
+
if (extra.action === 'phone') return 'phoneUrl';
|
|
65
|
+
if (extra.action === 'email') return 'emailUrl';
|
|
66
|
+
if (extra.cta) return extra.cta === 'primary' ? 'primaryCtaUrl' : `${extra.cta}Url`;
|
|
67
|
+
const fromText = toCamel([text || '', 'url']);
|
|
68
|
+
return fromText || 'ctaUrl';
|
|
69
|
+
}
|
|
70
|
+
if (kind === 'label' && extra.paired) {
|
|
71
|
+
if (extra.action === 'whatsapp') return 'whatsappLabel';
|
|
72
|
+
if (extra.action === 'phone') return 'phoneLabel';
|
|
73
|
+
if (extra.action === 'email') return 'emailLabel';
|
|
74
|
+
if (extra.cta) return extra.cta === 'primary' ? 'primaryCtaLabel' : `${extra.cta}Label`;
|
|
75
|
+
return toCamel([text || '', 'label']) || 'ctaLabel';
|
|
76
|
+
}
|
|
77
|
+
if (kind === 'image') return extra.alt ? toCamel([extra.alt, 'image']) || 'image' : 'image';
|
|
78
|
+
if (kind === 'alt') return extra.imageField ? extra.imageField.replace(/Image$/, 'ImageAlt').replace(/image$/, 'imageAlt') : 'imageAlt';
|
|
79
|
+
if (kind === 'placeholder') return toCamel([text || '', 'placeholder']) || 'placeholder';
|
|
80
|
+
|
|
81
|
+
const tagMap = {
|
|
82
|
+
h1: 'title',
|
|
83
|
+
h2: 'heading',
|
|
84
|
+
h3: 'subheading',
|
|
85
|
+
h4: 'subheading',
|
|
86
|
+
h5: 'label',
|
|
87
|
+
h6: 'label',
|
|
88
|
+
p: (text || '').length > 80 ? 'description' : 'subtitle',
|
|
89
|
+
CardTitle: 'title',
|
|
90
|
+
CardDescription: 'description',
|
|
91
|
+
Heading: 'title',
|
|
92
|
+
Title: 'title',
|
|
93
|
+
Subtitle: 'subtitle',
|
|
94
|
+
Description: 'description',
|
|
95
|
+
Typography: 'text',
|
|
96
|
+
Badge: 'badge',
|
|
97
|
+
button: 'label',
|
|
98
|
+
Button: 'label',
|
|
99
|
+
span: 'label',
|
|
100
|
+
li: 'item',
|
|
101
|
+
};
|
|
102
|
+
if (tagMap[tag]) {
|
|
103
|
+
const mapped = typeof tagMap[tag] === 'function' ? tagMap[tag] : tagMap[tag];
|
|
104
|
+
if (mapped === 'title' || mapped === 'heading' || mapped === 'subtitle' || mapped === 'description') {
|
|
105
|
+
return mapped;
|
|
106
|
+
}
|
|
107
|
+
const named = toCamel([text || '', mapped]);
|
|
108
|
+
return named || mapped;
|
|
109
|
+
}
|
|
110
|
+
return toCamel([text || kind || 'text']) || 'text';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function uniquePath(used, basePath) {
|
|
114
|
+
if (!used.has(basePath)) {
|
|
115
|
+
used.add(basePath);
|
|
116
|
+
return basePath;
|
|
117
|
+
}
|
|
118
|
+
let i = 2;
|
|
119
|
+
while (used.has(`${basePath}${i}`)) i++;
|
|
120
|
+
const next = `${basePath}${i}`;
|
|
121
|
+
used.add(next);
|
|
122
|
+
return next;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildFieldPath({ scope, section, field, used }) {
|
|
126
|
+
const resolvedScope = scope || 'home';
|
|
127
|
+
const parts = [resolvedScope];
|
|
128
|
+
// "about.about.title" reads worse than "about.title"; a section that merely
|
|
129
|
+
// repeats its own route adds no addressing value.
|
|
130
|
+
if (section && section !== resolvedScope) parts.push(section);
|
|
131
|
+
parts.push(field || 'text');
|
|
132
|
+
return uniquePath(used, parts.join('.'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function humanLabel(fieldPath) {
|
|
136
|
+
const last = String(fieldPath).split('.').pop() || fieldPath;
|
|
137
|
+
return last
|
|
138
|
+
.replace(/([A-Z])/g, ' $1')
|
|
139
|
+
.replace(/[-_]/g, ' ')
|
|
140
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
141
|
+
.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function classifyFieldType(kind, value) {
|
|
145
|
+
if (kind === 'image') return 'image';
|
|
146
|
+
if (kind === 'url') return 'url';
|
|
147
|
+
if (kind === 'email' || (typeof value === 'string' && /^mailto:/i.test(value))) return 'email';
|
|
148
|
+
if (kind === 'phone' || (typeof value === 'string' && /^(tel:|\+)/i.test(value))) return 'phone';
|
|
149
|
+
if (kind === 'color') return 'color';
|
|
150
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
151
|
+
if (typeof value === 'number') return 'number';
|
|
152
|
+
if (typeof value === 'string' && value.length > 80) return 'textarea';
|
|
153
|
+
if (typeof value === 'string' && /\$|lkr|usd|rs\.?\s*\d/i.test(value)) return 'currency';
|
|
154
|
+
return 'text';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
toCamel,
|
|
159
|
+
inferSection,
|
|
160
|
+
inferFieldName,
|
|
161
|
+
uniquePath,
|
|
162
|
+
buildFieldPath,
|
|
163
|
+
humanLabel,
|
|
164
|
+
classifyFieldType,
|
|
165
|
+
};
|