@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.
Files changed (53) hide show
  1. package/README.md +62 -114
  2. package/bin/index.js +101 -207
  3. package/package.json +20 -5
  4. package/src/arc/__fixtures__/next-app-basic/package.json +10 -0
  5. package/src/arc/__fixtures__/next-app-basic/src/app/globals.css +3 -0
  6. package/src/arc/__fixtures__/next-app-basic/src/app/layout.tsx +9 -0
  7. package/src/arc/__fixtures__/next-app-basic/src/app/page.tsx +11 -0
  8. package/src/arc/__fixtures__/next-app-basic/src/components/Header.tsx +13 -0
  9. package/src/arc/__fixtures__/next-app-basic/src/components/Hero.tsx +12 -0
  10. package/src/arc/__fixtures__/next-app-basic/src/components/PromoBanner.tsx +10 -0
  11. package/src/arc/__fixtures__/next-app-basic/tsconfig.json +12 -0
  12. package/src/arc/__fixtures__/next-app-storefront/components.json +14 -0
  13. package/src/arc/__fixtures__/next-app-storefront/package.json +22 -0
  14. package/src/arc/__fixtures__/next-app-storefront/src/app/about/page.tsx +13 -0
  15. package/src/arc/__fixtures__/next-app-storefront/src/app/globals.css +5 -0
  16. package/src/arc/__fixtures__/next-app-storefront/src/app/layout.tsx +19 -0
  17. package/src/arc/__fixtures__/next-app-storefront/src/app/page.tsx +13 -0
  18. package/src/arc/__fixtures__/next-app-storefront/src/components/Features.tsx +31 -0
  19. package/src/arc/__fixtures__/next-app-storefront/src/components/Hero.tsx +45 -0
  20. package/src/arc/__fixtures__/next-app-storefront/src/components/ProductGrid.tsx +49 -0
  21. package/src/arc/__fixtures__/next-app-storefront/src/components/SiteFooter.tsx +22 -0
  22. package/src/arc/__fixtures__/next-app-storefront/src/components/SiteHeader.tsx +21 -0
  23. package/src/arc/__fixtures__/next-app-storefront/src/components/ui/button.tsx +36 -0
  24. package/src/arc/__fixtures__/next-app-storefront/tsconfig.json +15 -0
  25. package/src/arc/__fixtures__/next-pages-basic/package.json +10 -0
  26. package/src/arc/__fixtures__/next-pages-basic/pages/_app.jsx +5 -0
  27. package/src/arc/__fixtures__/next-pages-basic/pages/contact.jsx +9 -0
  28. package/src/arc/__fixtures__/next-pages-basic/pages/index.jsx +11 -0
  29. package/src/arc/__fixtures__/next-pages-basic/styles/globals.css +9 -0
  30. package/src/arc/__tests__/arc.test.cjs +458 -0
  31. package/src/arc/adapters.cjs +184 -0
  32. package/src/arc/ast.cjs +323 -0
  33. package/src/arc/field-paths.cjs +165 -0
  34. package/src/arc/fivora-contract.cjs +521 -0
  35. package/src/arc/fs-utils.cjs +170 -0
  36. package/src/arc/index.cjs +628 -0
  37. package/src/arc/learning.cjs +185 -0
  38. package/src/arc/manifest.cjs +421 -0
  39. package/src/arc/next-config.cjs +279 -0
  40. package/src/arc/planner.cjs +227 -0
  41. package/src/arc/printer.cjs +153 -0
  42. package/src/arc/recipes-v2.cjs +49 -0
  43. package/src/arc/scanner.cjs +613 -0
  44. package/src/arc/semantic.cjs +651 -0
  45. package/src/arc/transformer.cjs +646 -0
  46. package/src/arc/validator.cjs +173 -0
  47. package/src/arc/version.cjs +22 -0
  48. package/src/recipes/cosmetics-beauty-store.json +1097 -0
  49. package/src/recipes/electronics-gadgets-store.json +1080 -0
  50. package/src/recipes/fashion-apparel-store.json +1074 -0
  51. package/src/tools/deneb-doctor.cjs +646 -0
  52. package/src/tools/recipe-engine.cjs +30 -0
  53. package/src/tools/template-converter.cjs +19 -6
@@ -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
+ };