@deneb-ui/cli 2.0.112 → 2.0.113
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 +46 -1
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +112 -5
- package/src/arc/adapters.cjs +33 -0
- package/src/arc/auto-editability-fixer.cjs +907 -0
- package/src/arc/component-registry.cjs +12 -0
- package/src/arc/field-paths.cjs +4 -1
- package/src/arc/fivora-schema-authority.cjs +9 -1
- package/src/arc/index.cjs +24 -0
- package/src/arc/planner.cjs +13 -0
- package/src/arc/residual.cjs +4 -2
- package/src/arc/semantic.cjs +200 -1
- package/src/arc/transforms/element-transform.cjs +35 -0
- package/src/arc/transforms/index.cjs +1 -0
- package/src/arc/universal-icon-patcher.cjs +220 -0
- package/src/tools/deneb-template-validator.cjs +1 -1
- package/src/tools/template-converter.cjs +76 -24
- package/src/tools/template-preview-focus-bridge.cjs +1 -1
package/bin/index.js
CHANGED
|
@@ -1276,6 +1276,18 @@ async function initProject(targetInput, options = {}) {
|
|
|
1276
1276
|
// Non-blocking doctor check
|
|
1277
1277
|
}
|
|
1278
1278
|
|
|
1279
|
+
// 7.5. Auto-Fix Editability (ensures all cards, text, and photos have field paths + site-data + recipe)
|
|
1280
|
+
try {
|
|
1281
|
+
const { runAutoEditabilityFix } = require('../src/arc/auto-editability-fixer.cjs');
|
|
1282
|
+
console.log('\n\x1b[36m⚡ Running DENEB auto-editability audit & field sync...\x1b[0m');
|
|
1283
|
+
const fixReport = runAutoEditabilityFix(targetDir, { dryRun: false, verbose: false });
|
|
1284
|
+
if (fixReport.fixed && fixReport.fixed.length > 0) {
|
|
1285
|
+
console.log(`\x1b[32m✔ Auto-fixed ${fixReport.fixed.length} editability targets (field paths, site-data, recipe)\x1b[0m`);
|
|
1286
|
+
}
|
|
1287
|
+
} catch (fixErr) {
|
|
1288
|
+
// Non-blocking auto-fix
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1279
1291
|
console.log(`\n\x1b[32m✔ Project initialization complete!\x1b[0m`);
|
|
1280
1292
|
console.log(`\nYou can now run:`);
|
|
1281
1293
|
console.log(` \x1b[36mnpm run lab\x1b[0m \x1b[90m# Launch Local Visual Editing Lab\x1b[0m`);
|
|
@@ -1666,6 +1678,34 @@ if (command === 'init' || command === 'arc') {
|
|
|
1666
1678
|
console.error(`\x1b[31m✖ Failed to save recipe:\x1b[0m ${err.message}`);
|
|
1667
1679
|
process.exit(1);
|
|
1668
1680
|
}
|
|
1681
|
+
} else if (command === 'fix-editability' || command === 'fix-fields') {
|
|
1682
|
+
let targetInput = '.';
|
|
1683
|
+
let dryRun = false;
|
|
1684
|
+
let verbose = false;
|
|
1685
|
+
for (let i = 0; i < commandArgs.length; i++) {
|
|
1686
|
+
const arg = commandArgs[i];
|
|
1687
|
+
if (arg === '--dry-run' || arg === '--dryrun') {
|
|
1688
|
+
dryRun = true;
|
|
1689
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
1690
|
+
verbose = true;
|
|
1691
|
+
} else if (!arg.startsWith('-')) {
|
|
1692
|
+
targetInput = arg;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const targetDir = path.resolve(process.cwd(), targetInput || '.');
|
|
1696
|
+
try {
|
|
1697
|
+
const { runFixEditability } = require('../src/arc/index.cjs');
|
|
1698
|
+
console.log(`\n\x1b[36m⚡ Running Deneb Auto-Editability Fixer on:\x1b[0m ${targetDir}`);
|
|
1699
|
+
const report = runFixEditability(targetDir, { dryRun, verbose });
|
|
1700
|
+
if (report.fixed.length > 0) {
|
|
1701
|
+
console.log(`\n\x1b[32m✔ Auto-fix completed! Updated ${report.fixed.length} target(s).\x1b[0m\n`);
|
|
1702
|
+
} else {
|
|
1703
|
+
console.log(`\n\x1b[32m✔ All cards, text, and photos are already fully editable!\x1b[0m\n`);
|
|
1704
|
+
}
|
|
1705
|
+
} catch (err) {
|
|
1706
|
+
console.error(`\x1b[31m✖ Auto-fix failed:\x1b[0m ${err.message}`);
|
|
1707
|
+
process.exit(1);
|
|
1708
|
+
}
|
|
1669
1709
|
} else {
|
|
1670
1710
|
console.log(`Usage: deneb <command> [options]
|
|
1671
1711
|
DENEB UI Framework — Powered by DENEB-UI Collaborate with FIVORA
|
|
@@ -1682,6 +1722,8 @@ Core Commands:
|
|
|
1682
1722
|
fonts Google Fonts catalog — list presets or install self-hosted @fontsource packages
|
|
1683
1723
|
update Update DENEB packages (@deneb-ui/ui, @deneb-ui/cli) and UI components
|
|
1684
1724
|
validate Validate website configuration and visual editing contracts with Fivora platform
|
|
1725
|
+
fix-editability Auto-fix missing field paths: patches components, populates site-data.json, updates recipe
|
|
1726
|
+
flags: --dry-run --verbose
|
|
1685
1727
|
zip Zip the project without unnecessary folders or files (node_modules, .next, .git, .env)
|
|
1686
1728
|
validate-and-zip Validate website configuration and immediately package clean upload-ready ZIP
|
|
1687
1729
|
|
|
@@ -1714,6 +1756,9 @@ Examples:
|
|
|
1714
1756
|
deneb validate-and-zip
|
|
1715
1757
|
deneb zip .
|
|
1716
1758
|
deneb add product-card
|
|
1717
|
-
deneb add all
|
|
1759
|
+
deneb add all
|
|
1760
|
+
deneb fix-editability .
|
|
1761
|
+
deneb fix-editability . --dry-run
|
|
1762
|
+
deneb fix-editability . --verbose`);
|
|
1718
1763
|
process.exit(1);
|
|
1719
1764
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.113",
|
|
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.113",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -225,6 +225,84 @@ test('AST transformer preserves className and uses nullish fallbacks', () => {
|
|
|
225
225
|
parseSource(result.code, relativeFile);
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
test('init marks a variable photo source as an editable image field', () => {
|
|
229
|
+
const relativeFile = 'src/components/DynamicHero.tsx';
|
|
230
|
+
const code = `
|
|
231
|
+
export function DynamicHero() {
|
|
232
|
+
const hero = { src: "/hero.webp" };
|
|
233
|
+
return <Image src={hero.src} alt="Lounge chair" className="rounded" />;
|
|
234
|
+
}
|
|
235
|
+
`;
|
|
236
|
+
const profile = scanProject(FIXTURE);
|
|
237
|
+
const analysis = analyzeFile({
|
|
238
|
+
code,
|
|
239
|
+
relativeFile,
|
|
240
|
+
profile,
|
|
241
|
+
graph: { sharedFiles: [] },
|
|
242
|
+
ownerScope: 'home',
|
|
243
|
+
componentMeta: { name: 'DynamicHero', role: 'hero' },
|
|
244
|
+
});
|
|
245
|
+
analysis.relativeFile = relativeFile;
|
|
246
|
+
analysis.code = code;
|
|
247
|
+
const image = analysis.candidates.find((candidate) => candidate.reason === 'dynamic-image-source');
|
|
248
|
+
assert.ok(image, 'expected a dynamic image candidate');
|
|
249
|
+
const plan = planTransformations({ profile, analyses: [analysis] });
|
|
250
|
+
const result = applyFilePlan(plan.files[0], profile);
|
|
251
|
+
assert.match(result.code, /data-preview-field-path="home\.hero\.heroImage"/);
|
|
252
|
+
assert.match(result.code, /hero\.src/);
|
|
253
|
+
assert.match(result.code, /\|\|/);
|
|
254
|
+
parseSource(result.code, relativeFile);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('init does not assign one shared photo field inside a reusable image component', () => {
|
|
258
|
+
const relativeFile = 'src/components/VehicleImage.tsx';
|
|
259
|
+
const code = `
|
|
260
|
+
export function VehicleImage({ image }) {
|
|
261
|
+
const src = image?.src || "/placeholder.svg";
|
|
262
|
+
return <Image src={src} alt="" />;
|
|
263
|
+
}
|
|
264
|
+
`;
|
|
265
|
+
const profile = scanProject(FIXTURE);
|
|
266
|
+
const analysis = analyzeFile({
|
|
267
|
+
code,
|
|
268
|
+
relativeFile,
|
|
269
|
+
profile,
|
|
270
|
+
graph: { sharedFiles: [] },
|
|
271
|
+
ownerScope: 'common',
|
|
272
|
+
componentMeta: { name: 'VehicleImage', role: 'card' },
|
|
273
|
+
});
|
|
274
|
+
const image = analysis.candidates.find((candidate) => candidate.reason === 'dynamic-image-source');
|
|
275
|
+
assert.equal(image, undefined);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('init marks a home page photo passed in as a prop', () => {
|
|
279
|
+
const relativeFile = 'src/components/Hero.tsx';
|
|
280
|
+
const code = `
|
|
281
|
+
export function Hero({ image }) {
|
|
282
|
+
return <Image src={image.src} alt="Showroom" />;
|
|
283
|
+
}
|
|
284
|
+
`;
|
|
285
|
+
const profile = scanProject(FIXTURE);
|
|
286
|
+
const analysis = analyzeFile({
|
|
287
|
+
code,
|
|
288
|
+
relativeFile,
|
|
289
|
+
profile,
|
|
290
|
+
graph: { sharedFiles: [] },
|
|
291
|
+
ownerScope: 'home',
|
|
292
|
+
componentMeta: { name: 'Hero', role: 'hero' },
|
|
293
|
+
});
|
|
294
|
+
analysis.relativeFile = relativeFile;
|
|
295
|
+
analysis.code = code;
|
|
296
|
+
const image = analysis.candidates.find((candidate) => candidate.reason === 'dynamic-image-source');
|
|
297
|
+
assert.ok(image, 'expected a home photo candidate');
|
|
298
|
+
const plan = planTransformations({ profile, analyses: [analysis] });
|
|
299
|
+
const result = applyFilePlan(plan.files[0], profile);
|
|
300
|
+
assert.match(result.code, /data-preview-field-path="home\.hero\.image"/);
|
|
301
|
+
assert.match(result.code, /image\.src/);
|
|
302
|
+
assert.match(result.code, /\|\|/);
|
|
303
|
+
parseSource(result.code, relativeFile);
|
|
304
|
+
});
|
|
305
|
+
|
|
228
306
|
test('planner emits style-bind next to content transforms', () => {
|
|
229
307
|
const relativeFile = 'src/components/Hero.tsx';
|
|
230
308
|
const code = fs.readFileSync(path.join(FIXTURE, relativeFile), 'utf8');
|
|
@@ -3953,9 +4031,38 @@ test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remed
|
|
|
3953
4031
|
}
|
|
3954
4032
|
});
|
|
3955
4033
|
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
4034
|
+
test('ARC Component Registry recognizes EditableUniversalCard and multi-industry card aliases', () => {
|
|
4035
|
+
const { isKnownComponent, classifyComponents } = require('../component-registry.cjs');
|
|
4036
|
+
assert.equal(isKnownComponent('EditableUniversalCard'), true);
|
|
4037
|
+
assert.equal(isKnownComponent('UniversalCard'), true);
|
|
4038
|
+
assert.equal(isKnownComponent('ListingCard'), true);
|
|
4039
|
+
assert.equal(isKnownComponent('EntityCard'), true);
|
|
4040
|
+
assert.equal(isKnownComponent('EditableVehicleCard'), true);
|
|
4041
|
+
assert.equal(isKnownComponent('VehicleCard'), true);
|
|
4042
|
+
|
|
4043
|
+
const res = classifyComponents(['UniversalCard', 'VehicleCard', 'UnknownCustomWidget']);
|
|
4044
|
+
assert.deepEqual(res.known, ['UniversalCard', 'VehicleCard']);
|
|
4045
|
+
assert.deepEqual(res.unknown, ['UnknownCustomWidget']);
|
|
4046
|
+
});
|
|
4047
|
+
|
|
4048
|
+
test('ARC auto-editability fixer handles multi-industry card collections (books, gym plans, restaurants, vehicles)', () => {
|
|
4049
|
+
const { buildGenericCardRecipeSection } = require('../auto-editability-fixer.cjs');
|
|
4050
|
+
const bookCollection = {
|
|
4051
|
+
'book-1': {
|
|
4052
|
+
title: 'To Kill a Mockingbird',
|
|
4053
|
+
author: 'Harper Lee',
|
|
4054
|
+
year: '1960',
|
|
4055
|
+
price: '$18.50',
|
|
4056
|
+
},
|
|
4057
|
+
};
|
|
4058
|
+
const section = buildGenericCardRecipeSection('books', bookCollection);
|
|
4059
|
+
assert.equal(section.id, 'cards.books');
|
|
4060
|
+
assert.equal(section.type, 'collection');
|
|
4061
|
+
assert.equal(section.items.length, 1);
|
|
4062
|
+
assert.equal(section.items[0].key, 'book-1');
|
|
4063
|
+
const keys = section.items[0].fields.map((f) => f.key);
|
|
4064
|
+
assert.ok(keys.includes('title'));
|
|
4065
|
+
assert.ok(keys.includes('author'));
|
|
4066
|
+
assert.ok(keys.includes('price'));
|
|
4067
|
+
});
|
|
3961
4068
|
|
package/src/arc/adapters.cjs
CHANGED
|
@@ -31,6 +31,20 @@ const FORM_CONTAINER_TAGS = new Set(['form', 'Form']);
|
|
|
31
31
|
|
|
32
32
|
const ADAPTERS = BUILTIN_ADAPTERS;
|
|
33
33
|
|
|
34
|
+
const UTILITY_ICON_PATTERNS = [
|
|
35
|
+
/^(Chevron|Arrow|Move|Caret|Angle|Sort|Chevrons)/i,
|
|
36
|
+
/^(X|Close|Menu|MenuAlt|Hamburger)/i,
|
|
37
|
+
/^(Loader|Spinner)/i,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function isUtilityIcon(name) {
|
|
41
|
+
if (!name) return false;
|
|
42
|
+
const clean = String(name).replace(/Icon$/i, '');
|
|
43
|
+
if (UTILITY_ICON_PATTERNS.some((re) => re.test(clean))) return true;
|
|
44
|
+
const lower = clean.toLowerCase();
|
|
45
|
+
return ['x', 'close', 'menu', 'spinner', 'loader'].includes(lower);
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
function isIconComponent(name, importSource) {
|
|
35
49
|
if (!name) return false;
|
|
36
50
|
if (DECORATIVE_TAGS.has(name)) return true;
|
|
@@ -39,6 +53,22 @@ function isIconComponent(name, importSource) {
|
|
|
39
53
|
return false;
|
|
40
54
|
}
|
|
41
55
|
|
|
56
|
+
function isFeatureIcon(name, importSource) {
|
|
57
|
+
if (!name) return false;
|
|
58
|
+
if (DECORATIVE_TAGS.has(name)) return false;
|
|
59
|
+
if (!isIconComponent(name, importSource)) return false;
|
|
60
|
+
return !isUtilityIcon(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractIconBaseName(name) {
|
|
64
|
+
if (!name) return 'circle';
|
|
65
|
+
let clean = String(name).replace(/Icon$/i, '');
|
|
66
|
+
return clean
|
|
67
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
function classifyHref(href) {
|
|
43
73
|
const value = String(href || '');
|
|
44
74
|
if (/wa\.me|whatsapp/i.test(value)) return 'whatsapp';
|
|
@@ -157,6 +187,9 @@ module.exports = {
|
|
|
157
187
|
recognizeWithAdapters,
|
|
158
188
|
resolveActionWithAdapters,
|
|
159
189
|
isIconComponent,
|
|
190
|
+
isUtilityIcon,
|
|
191
|
+
isFeatureIcon,
|
|
192
|
+
extractIconBaseName,
|
|
160
193
|
classifyHref,
|
|
161
194
|
classifyActionIntent,
|
|
162
195
|
isLikelyCtaClass,
|