@deneb-ui/cli 2.0.46 → 2.0.47
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 +75 -0
- package/src/arc/ast.cjs +8 -1
- package/src/arc/field-paths.cjs +5 -1
- package/src/arc/transformer.cjs +64 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.47",
|
|
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.47",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -626,3 +626,78 @@ export { useSiteData, contentText };
|
|
|
626
626
|
assert.match(out, /export\s*\{[^}]*useSiteData[^}]*\}\s*from\s*['"]@deneb-ui\/ui['"]/);
|
|
627
627
|
assert.doesNotMatch(out, /import\s*\{[^}]*useSiteData/);
|
|
628
628
|
});
|
|
629
|
+
|
|
630
|
+
test('toCamel and optionalMember safely handle strings starting with numbers without syntax errors', () => {
|
|
631
|
+
const { toCamel } = require('../field-paths.cjs');
|
|
632
|
+
const { optionalMember, parseSource } = require('../ast.cjs');
|
|
633
|
+
const recast = require('recast');
|
|
634
|
+
|
|
635
|
+
// toCamel prefixes identifiers starting with a number
|
|
636
|
+
assert.equal(toCamel(['1800840AURA']), 'item1800840aura');
|
|
637
|
+
assert.equal(toCamel(['25-Min', 'Express']), 'item25MinExpress');
|
|
638
|
+
|
|
639
|
+
// optionalMember safely falls back to computed string literal for non-identifier keys
|
|
640
|
+
const chain = optionalMember(['siteData', 'content', 'home', '100EncryptedLabel']);
|
|
641
|
+
const code = recast.print(chain).code;
|
|
642
|
+
assert.equal(code, 'siteData?.content?.home?.["100EncryptedLabel"]');
|
|
643
|
+
|
|
644
|
+
// Verify it parses as valid JS without syntax errors
|
|
645
|
+
assert.doesNotThrow(() => parseSource(`const x = ${code};`, 'test.tsx'));
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('bindArrayDeclaration transforms module-scope array into DEFAULT_ fallback in client components', () => {
|
|
649
|
+
const clientCode = `'use client';
|
|
650
|
+
import React from 'react';
|
|
651
|
+
|
|
652
|
+
const BRANDS = [
|
|
653
|
+
{ name: 'Apple' },
|
|
654
|
+
{ name: 'Samsung' },
|
|
655
|
+
];
|
|
656
|
+
|
|
657
|
+
export function BrandMarquee() {
|
|
658
|
+
return (
|
|
659
|
+
<div>
|
|
660
|
+
{BRANDS.map((b) => (
|
|
661
|
+
<div key={b.name} className="brand-item">
|
|
662
|
+
<span>{b.name}</span>
|
|
663
|
+
</div>
|
|
664
|
+
))}
|
|
665
|
+
</div>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
`;
|
|
669
|
+
|
|
670
|
+
const profile = {
|
|
671
|
+
root: os.tmpdir(),
|
|
672
|
+
framework: 'nextjs',
|
|
673
|
+
router: 'next-app',
|
|
674
|
+
language: 'typescript',
|
|
675
|
+
cssSystems: ['tailwind'],
|
|
676
|
+
hasSrc: true,
|
|
677
|
+
aliasMap: { '@/*': ['src/*'] },
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const analysis = analyzeFile({
|
|
681
|
+
code: clientCode,
|
|
682
|
+
relativeFile: 'src/components/BrandMarquee.tsx',
|
|
683
|
+
profile,
|
|
684
|
+
graph: { sharedFiles: [] },
|
|
685
|
+
ownerScope: 'home',
|
|
686
|
+
componentMeta: { name: 'BrandMarquee', role: 'about' },
|
|
687
|
+
});
|
|
688
|
+
analysis.code = clientCode;
|
|
689
|
+
analysis.relativeFile = 'src/components/BrandMarquee.tsx';
|
|
690
|
+
|
|
691
|
+
const plan = planTransformations({ profile, analyses: [analysis] });
|
|
692
|
+
const result = applyFilePlan(plan.files[0], profile);
|
|
693
|
+
|
|
694
|
+
assert.equal(result.changed, true);
|
|
695
|
+
// Module-scope array renamed to DEFAULT_BRANDS with literal array preserved
|
|
696
|
+
assert.match(result.code, /const DEFAULT_BRANDS = \[\s*\{\s*name:\s*['"]Apple['"]\s*\}/);
|
|
697
|
+
// Inside component body: useSiteData hook followed by dynamic BRANDS binding
|
|
698
|
+
assert.match(result.code, /const siteData = useSiteData\(\);/);
|
|
699
|
+
assert.match(result.code, /const BRANDS = siteData\?\.content\?\.home\?\.BRANDS \?\? DEFAULT_BRANDS;/);
|
|
700
|
+
// Verify AST parses cleanly
|
|
701
|
+
assert.doesNotThrow(() => parseSource(result.code, 'BrandMarquee.tsx'));
|
|
702
|
+
});
|
|
703
|
+
|
package/src/arc/ast.cjs
CHANGED
|
@@ -158,10 +158,17 @@ function isJsxTextHeavy(node) {
|
|
|
158
158
|
return meaningful.length > 0;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
const VALID_IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
162
|
+
|
|
161
163
|
function optionalMember(parts) {
|
|
162
164
|
let expr = b.identifier(parts[0]);
|
|
163
165
|
for (let i = 1; i < parts.length; i++) {
|
|
164
|
-
|
|
166
|
+
const part = String(parts[i]);
|
|
167
|
+
if (VALID_IDENTIFIER_REGEX.test(part)) {
|
|
168
|
+
expr = b.optionalMemberExpression(expr, b.identifier(part), false, true);
|
|
169
|
+
} else {
|
|
170
|
+
expr = b.optionalMemberExpression(expr, b.stringLiteral(part), true, true);
|
|
171
|
+
}
|
|
165
172
|
}
|
|
166
173
|
return expr;
|
|
167
174
|
}
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -15,13 +15,17 @@ function toCamel(parts) {
|
|
|
15
15
|
.filter((w, i) => i === 0 || !STOP_WORDS.has(w.toLowerCase()))
|
|
16
16
|
.slice(0, 5);
|
|
17
17
|
if (!cleaned.length) return '';
|
|
18
|
-
|
|
18
|
+
const camel = cleaned
|
|
19
19
|
.map((word, i) => {
|
|
20
20
|
const lower = word.toLowerCase();
|
|
21
21
|
if (i === 0) return lower;
|
|
22
22
|
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
23
23
|
})
|
|
24
24
|
.join('');
|
|
25
|
+
if (/^[0-9]/.test(camel)) {
|
|
26
|
+
return 'item' + camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
27
|
+
}
|
|
28
|
+
return camel;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
function inferSection(context) {
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -238,7 +238,7 @@ function wrapLiteralTextChildren(node, fieldPath, fallback) {
|
|
|
238
238
|
* emitted as JSX template literals (`items[${index}].title`) so added and
|
|
239
239
|
* reordered items stay editable, which is what strict mode requires.
|
|
240
240
|
*/
|
|
241
|
-
function applyCollectionTransform(ast, transform) {
|
|
241
|
+
function applyCollectionTransform(ast, transform, isClient = false) {
|
|
242
242
|
const listPath = transform.listField;
|
|
243
243
|
const binding = transform.itemParam;
|
|
244
244
|
if (!listPath || !binding) return false;
|
|
@@ -290,7 +290,7 @@ function applyCollectionTransform(ast, transform) {
|
|
|
290
290
|
);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
return bindArrayDeclaration(ast, mapCall, listPath);
|
|
293
|
+
return bindArrayDeclaration(ast, mapCall, listPath, isClient);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
function findMapCall(ast, loc) {
|
|
@@ -376,7 +376,40 @@ function findListContainer(mapCallPath) {
|
|
|
376
376
|
return null;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
function
|
|
379
|
+
function isModuleLevel(pathNode) {
|
|
380
|
+
let current = pathNode.parentPath || pathNode.parent;
|
|
381
|
+
while (current) {
|
|
382
|
+
const type = current.node?.type;
|
|
383
|
+
if (
|
|
384
|
+
type === 'FunctionDeclaration' ||
|
|
385
|
+
type === 'FunctionExpression' ||
|
|
386
|
+
type === 'ArrowFunctionExpression'
|
|
387
|
+
) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
if (type === 'Program') return true;
|
|
391
|
+
current = current.parentPath || current.parent;
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function findEnclosingFunction(pathNode) {
|
|
397
|
+
let current = pathNode.parentPath || pathNode.parent;
|
|
398
|
+
while (current) {
|
|
399
|
+
const type = current.node?.type;
|
|
400
|
+
if (
|
|
401
|
+
type === 'FunctionDeclaration' ||
|
|
402
|
+
type === 'FunctionExpression' ||
|
|
403
|
+
type === 'ArrowFunctionExpression'
|
|
404
|
+
) {
|
|
405
|
+
return current;
|
|
406
|
+
}
|
|
407
|
+
current = current.parentPath || current.parent;
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function bindArrayDeclaration(ast, mapCallPath, listPath, isClient = false) {
|
|
380
413
|
const arrayName = mapCallPath.node.callee.object?.name;
|
|
381
414
|
if (!arrayName) return false;
|
|
382
415
|
let bound = false;
|
|
@@ -392,6 +425,33 @@ function bindArrayDeclaration(ast, mapCallPath, listPath) {
|
|
|
392
425
|
this.traverse(pathNode);
|
|
393
426
|
return;
|
|
394
427
|
}
|
|
428
|
+
|
|
429
|
+
if (isClient && isModuleLevel(pathNode)) {
|
|
430
|
+
const defaultName = 'DEFAULT_' + arrayName;
|
|
431
|
+
node.id.name = defaultName;
|
|
432
|
+
const fnPath = findEnclosingFunction(mapCallPath);
|
|
433
|
+
if (fnPath && fnPath.node.body?.type === 'BlockStatement') {
|
|
434
|
+
const body = fnPath.node.body.body;
|
|
435
|
+
const already = body.some((stmt) => recast.print(stmt).code.includes(`const ${arrayName} =`));
|
|
436
|
+
if (!already) {
|
|
437
|
+
const localDecl = b.variableDeclaration('const', [
|
|
438
|
+
b.variableDeclarator(
|
|
439
|
+
b.identifier(arrayName),
|
|
440
|
+
siteDataListBinding(listPath.split('.'), b.identifier(defaultName))
|
|
441
|
+
),
|
|
442
|
+
]);
|
|
443
|
+
const hookIdx = body.findIndex((stmt) => recast.print(stmt).code.includes('useSiteData'));
|
|
444
|
+
if (hookIdx >= 0) {
|
|
445
|
+
body.splice(hookIdx + 1, 0, localDecl);
|
|
446
|
+
} else {
|
|
447
|
+
body.unshift(localDecl);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
bound = true;
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
395
455
|
node.init = siteDataListBinding(listPath.split('.'), node.init);
|
|
396
456
|
bound = true;
|
|
397
457
|
return false;
|
|
@@ -549,7 +609,7 @@ function applyFilePlan(filePlan, profile) {
|
|
|
549
609
|
|
|
550
610
|
if (transform.operation === 'collection-conversion') {
|
|
551
611
|
try {
|
|
552
|
-
if (applyCollectionTransform(ast, transform)) applied++;
|
|
612
|
+
if (applyCollectionTransform(ast, transform, isClient)) applied++;
|
|
553
613
|
else failures.push({ loc: transform.loc, reason: 'collection-not-bindable' });
|
|
554
614
|
} catch (err) {
|
|
555
615
|
failures.push({ loc: transform.loc, reason: err.message });
|