@deneb-ui/cli 2.0.23 → 2.0.25
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 +18 -0
- package/package.json +6 -1
- package/src/arc/__tests__/arc.test.cjs +46 -6
- package/src/arc/ast.cjs +17 -1
- package/src/arc/font-plan.cjs +65 -0
- package/src/arc/index.cjs +5 -6
- package/src/arc/learning.cjs +26 -0
- package/src/arc/manifest.cjs +17 -0
- package/src/arc/planner.cjs +26 -1
- package/src/arc/printer.cjs +32 -2
- package/src/arc/style-candidates.cjs +72 -0
- package/src/arc/transformer.cjs +57 -1
- package/src/arc/version.cjs +1 -1
- package/src/common/style-validation.ts +30 -0
- package/src/tools/deneb-fonts.cjs +114 -0
- package/src/tools/template-preview-focus-bridge.cjs +3 -3
package/bin/index.js
CHANGED
|
@@ -692,6 +692,7 @@ function initProject(targetInput, options = {}) {
|
|
|
692
692
|
'validate-and-zip': 'deneb validate-and-zip .',
|
|
693
693
|
'package:template': 'deneb package .',
|
|
694
694
|
'update:deneb': 'deneb update',
|
|
695
|
+
'fonts:install': 'deneb fonts install .',
|
|
695
696
|
};
|
|
696
697
|
let addedCount = 0;
|
|
697
698
|
for (const [key, val] of Object.entries(scriptsToAdd)) {
|
|
@@ -763,6 +764,16 @@ function initProject(targetInput, options = {}) {
|
|
|
763
764
|
}
|
|
764
765
|
}
|
|
765
766
|
|
|
767
|
+
if (!skipInstall) {
|
|
768
|
+
try {
|
|
769
|
+
const { runFontsInstall } = require('../src/tools/deneb-fonts.cjs');
|
|
770
|
+
console.log(`\n\x1b[36mAa Downloading and configuring DENEB Google Fonts...\x1b[0m`);
|
|
771
|
+
runFontsInstall(targetDir, []);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
console.log(`\n\x1b[33m! Fonts were not installed automatically: ${err.message}. Run \x1b[1mdeneb fonts install .\x1b[0m\x1b[0m`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
766
777
|
console.log(`\n\x1b[32m✔ Project initialization complete!\x1b[0m`);
|
|
767
778
|
console.log(`\nYou can now run:`);
|
|
768
779
|
console.log(` \x1b[36mnpm run lab\x1b[0m \x1b[90m# Launch Local Visual Editing Lab\x1b[0m`);
|
|
@@ -1074,6 +1085,9 @@ if (command === 'init') {
|
|
|
1074
1085
|
runValidateAndZip(commandArgs[0] || '.', commandArgs.slice(1));
|
|
1075
1086
|
} else if (command === 'update' || command === 'upgrade') {
|
|
1076
1087
|
updateDependencies(commandArgs);
|
|
1088
|
+
} else if (command === 'fonts') {
|
|
1089
|
+
const { runFontsCommand } = require('../src/tools/deneb-fonts.cjs');
|
|
1090
|
+
process.exit(runFontsCommand(commandArgs));
|
|
1077
1091
|
} else if (command === 'save-recipe' || command === 'learn') {
|
|
1078
1092
|
const targetDir = path.resolve(commandArgs[0] || '.');
|
|
1079
1093
|
const recipeName = commandArgs[1] || path.basename(targetDir);
|
|
@@ -1098,6 +1112,7 @@ Core Commands:
|
|
|
1098
1112
|
doctor Run comprehensive environment, manifest, visual editing AST & asset diagnostic checks (flags: --fix, --json)
|
|
1099
1113
|
save-recipe Learn and save calibrated fixes & schemas into reusable recipe bank (e.g. deneb save-recipe . shoes-store)
|
|
1100
1114
|
learn Alias for save-recipe
|
|
1115
|
+
fonts Google Fonts catalog — list presets or install self-hosted @fontsource packages
|
|
1101
1116
|
update Update DENEB packages (@deneb-ui/ui, @deneb-ui/cli) and UI components
|
|
1102
1117
|
validate Validate website configuration and visual editing contracts with Fivora platform
|
|
1103
1118
|
zip Zip the project without unnecessary folders or files (node_modules, .next, .git, .env)
|
|
@@ -1121,6 +1136,9 @@ Examples:
|
|
|
1121
1136
|
deneb init --recipe electronics
|
|
1122
1137
|
deneb init --recipe cosmetics
|
|
1123
1138
|
deneb init --legacy
|
|
1139
|
+
deneb fonts list
|
|
1140
|
+
deneb fonts install .
|
|
1141
|
+
deneb fonts install . --font inter --font playfair-display
|
|
1124
1142
|
deneb update
|
|
1125
1143
|
deneb validate .
|
|
1126
1144
|
deneb validate --zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.25",
|
|
4
4
|
"description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deneb": "bin/index.js",
|
|
@@ -48,7 +48,12 @@
|
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@babel/parser": "^7.28.0",
|
|
51
|
+
"@deneb-ui/core": "^2.0.25",
|
|
51
52
|
"adm-zip": "^0.6.0",
|
|
52
53
|
"recast": "^0.23.11"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"esbuild": "^0.28.2",
|
|
57
|
+
"typescript": "^5.9.3"
|
|
53
58
|
}
|
|
54
59
|
}
|
|
@@ -13,6 +13,13 @@ const { applyFilePlan } = require('../transformer.cjs');
|
|
|
13
13
|
const { buildFieldPath, inferFieldName } = require('../field-paths.cjs');
|
|
14
14
|
const { runDenebArc } = require('../index.cjs');
|
|
15
15
|
const { parseSource } = require('../ast.cjs');
|
|
16
|
+
const { loadFingerprintBoost } = require('../learning.cjs');
|
|
17
|
+
|
|
18
|
+
test('unseen fingerprints do not change planner confidence', () => {
|
|
19
|
+
const hint = loadFingerprintBoost(null);
|
|
20
|
+
assert.equal(hint.boost, 0);
|
|
21
|
+
assert.equal(hint.skip, false);
|
|
22
|
+
});
|
|
16
23
|
const contract = require('../fivora-contract.cjs');
|
|
17
24
|
const { ensureStaticExportConfig } = require('../next-config.cjs');
|
|
18
25
|
|
|
@@ -101,12 +108,38 @@ test('AST transformer preserves className and uses nullish fallbacks', () => {
|
|
|
101
108
|
assert.match(result.code, /className="hero"/);
|
|
102
109
|
assert.match(result.code, /data-preview-field-path=/);
|
|
103
110
|
assert.match(result.code, /\?\?/);
|
|
104
|
-
assert.match(result.code, /<span
|
|
111
|
+
assert.match(result.code, /<span[\s\S]*data-preview-field-path="/);
|
|
105
112
|
assert.doesNotMatch(result.code, /'use client'/);
|
|
106
113
|
assert.match(result.code, /site-data\.json|@\/data\/site-data\.json/);
|
|
114
|
+
assert.match(result.code, /data-preview-style-target=/);
|
|
115
|
+
assert.match(result.code, /data-preview-style-type="text"/);
|
|
107
116
|
parseSource(result.code, relativeFile);
|
|
108
117
|
});
|
|
109
118
|
|
|
119
|
+
test('planner emits style-bind next to content transforms', () => {
|
|
120
|
+
const relativeFile = 'src/components/Hero.tsx';
|
|
121
|
+
const code = fs.readFileSync(path.join(FIXTURE, relativeFile), 'utf8');
|
|
122
|
+
const analysis = analyzeFile({
|
|
123
|
+
code,
|
|
124
|
+
relativeFile,
|
|
125
|
+
profile: scanProject(FIXTURE),
|
|
126
|
+
graph: { sharedFiles: [] },
|
|
127
|
+
ownerScope: 'home',
|
|
128
|
+
componentMeta: { name: 'Hero', role: 'hero' },
|
|
129
|
+
});
|
|
130
|
+
analysis.relativeFile = relativeFile;
|
|
131
|
+
analysis.code = code;
|
|
132
|
+
const plan = planTransformations({
|
|
133
|
+
profile: scanProject(FIXTURE),
|
|
134
|
+
analyses: [analysis],
|
|
135
|
+
recipe: { actionRules: { splitActionAndLabel: true } },
|
|
136
|
+
});
|
|
137
|
+
const binds = plan.files[0].transformations.filter((t) => t.operation === 'style-bind');
|
|
138
|
+
assert.ok(binds.length >= 1, 'expected style-bind operations');
|
|
139
|
+
assert.ok(binds.every((t) => t.stylePath && t.styleKind));
|
|
140
|
+
assert.ok(plan.stats.styleBinds >= 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
110
143
|
test('dry-run does not modify source files', () => {
|
|
111
144
|
const dir = copyFixture();
|
|
112
145
|
const before = fs.readFileSync(path.join(dir, 'src', 'components', 'Hero.tsx'), 'utf8');
|
|
@@ -142,6 +175,11 @@ test('ARC converts a Next.js fixture into Fivora contracts without redesigning',
|
|
|
142
175
|
assert.doesNotMatch(promo, /use client';;/);
|
|
143
176
|
assert.doesNotMatch(JSON.stringify(siteData.content), /VANTA/);
|
|
144
177
|
assert.ok(siteData.content.home.hero.title);
|
|
178
|
+
assert.ok(siteData.theme?.headingFont);
|
|
179
|
+
assert.ok(siteData.theme?.bodyFont);
|
|
180
|
+
assert.equal(typeof siteData.styles, 'object');
|
|
181
|
+
assert.ok(Object.keys(siteData.styles).length >= 1, 'style-bind seeds site-data.styles');
|
|
182
|
+
assert.match(hero, /data-preview-style-type=/);
|
|
145
183
|
assert.ok(manifest.editorSchema.sections.length >= 1);
|
|
146
184
|
assert.ok(manifest.editorSchema.sections.length < 12);
|
|
147
185
|
assert.equal(manifest.arcVersion, result.arcVersion);
|
|
@@ -150,8 +188,8 @@ test('ARC converts a Next.js fixture into Fivora contracts without redesigning',
|
|
|
150
188
|
const second = silence(() => runDenebArc(dir, 'arc-fixture', { telemetry: 'off' }));
|
|
151
189
|
assert.equal(second.outcome, 'success');
|
|
152
190
|
const hero2 = fs.readFileSync(path.join(dir, 'src', 'components', 'Hero.tsx'), 'utf8');
|
|
153
|
-
const spanCount1 = (hero.match(/<span
|
|
154
|
-
const spanCount2 = (hero2.match(/<span
|
|
191
|
+
const spanCount1 = (hero.match(/<span[\s\S]*?data-preview-field-path=/g) || []).length;
|
|
192
|
+
const spanCount2 = (hero2.match(/<span[\s\S]*?data-preview-field-path=/g) || []).length;
|
|
155
193
|
assert.equal(spanCount2, spanCount1);
|
|
156
194
|
});
|
|
157
195
|
|
|
@@ -301,6 +339,8 @@ test('static-array collections become list contracts without changing render log
|
|
|
301
339
|
const grid = fs.readFileSync(path.join(dir, 'src', 'components', 'ProductGrid.tsx'), 'utf8');
|
|
302
340
|
|
|
303
341
|
assert.match(grid, /data-preview-list-path="home\.products"/);
|
|
342
|
+
assert.match(grid, /data-preview-style-type="grid"/);
|
|
343
|
+
assert.match(grid, /data-preview-style-type="card"/);
|
|
304
344
|
assert.match(grid, /data-preview-item-path=\{`home\.products\[\$\{index\}\]`\}/);
|
|
305
345
|
assert.match(grid, /data-preview-field-path=\{`home\.products\[\$\{index\}\]\.title`\}/);
|
|
306
346
|
// The array is site-data backed with the developer's literal as fallback.
|
|
@@ -332,7 +372,7 @@ test('literal text inside a broad container is wrapped instead of marked illegal
|
|
|
332
372
|
const about = fs.readFileSync(path.join(dir, 'src', 'app', 'about', 'page.tsx'), 'utf8');
|
|
333
373
|
|
|
334
374
|
// Fivora rejects data-preview-field-path on <div>, so the text gets a span.
|
|
335
|
-
assert.match(about, /<div
|
|
375
|
+
assert.match(about, /<div[\s\S]*className="mt-10 text-sm text-slate-500"[\s\S]*<span[\s\S]*data-preview-field-path=/);
|
|
336
376
|
assert.ok(!/<div[^>]*data-preview-field-path/.test(about));
|
|
337
377
|
});
|
|
338
378
|
|
|
@@ -343,7 +383,7 @@ test('shadcn Button asChild keeps the action on the link and the label in a span
|
|
|
343
383
|
|
|
344
384
|
assert.match(hero, /<Button asChild>/);
|
|
345
385
|
assert.match(hero, /href=\{siteData\?\.content\?\.home\?\.hero\?\.shopCollectionUrl \?\? "\/products"\}/);
|
|
346
|
-
assert.match(hero, /<span
|
|
386
|
+
assert.match(hero, /<span[\s\S]*data-preview-field-path="home\.hero\.shopCollectionLabel"/);
|
|
347
387
|
// The decorative icon stays static.
|
|
348
388
|
assert.match(hero, /<ArrowRight className="ml-2 size-4" aria-hidden="true" \/>/);
|
|
349
389
|
});
|
|
@@ -385,7 +425,7 @@ test('Pages Router projects convert and satisfy the strict contract', () => {
|
|
|
385
425
|
assert.match(index, /from "\.\.\/data\/site-data\.json"/);
|
|
386
426
|
// tel: actions are split into url + label.
|
|
387
427
|
assert.match(index, /href=\{siteData\?\.content\?\.home\?\.contact\?\.phoneUrl \?\? "tel:\+94771234567"\}/);
|
|
388
|
-
assert.match(index, /<span
|
|
428
|
+
assert.match(index, /<span[\s\S]*data-preview-field-path="home\.contact\.phoneLabel"/);
|
|
389
429
|
// Original class names are untouched.
|
|
390
430
|
assert.match(index, /className="wrapper"/);
|
|
391
431
|
assert.match(contact, /className="photo"/);
|
package/src/arc/ast.cjs
CHANGED
|
@@ -229,7 +229,7 @@ function wrapTextInEditableSpan(fieldPath, fallback, fieldType) {
|
|
|
229
229
|
return b.jsxElement(
|
|
230
230
|
b.jsxOpeningElement(
|
|
231
231
|
b.jsxIdentifier('span'),
|
|
232
|
-
[jsxPreviewAttr(fieldPath)],
|
|
232
|
+
[jsxPreviewAttr(fieldPath), ...jsxStyleAttrs(fieldPath, 'text')],
|
|
233
233
|
false
|
|
234
234
|
),
|
|
235
235
|
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
@@ -238,6 +238,20 @@ function wrapTextInEditableSpan(fieldPath, fallback, fieldType) {
|
|
|
238
238
|
);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
function jsxStyleAttrs(stylePath, kind) {
|
|
242
|
+
if (!stylePath || !kind) return [];
|
|
243
|
+
return [
|
|
244
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-style-target'), b.stringLiteral(stylePath)),
|
|
245
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-style-type'), b.stringLiteral(kind)),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ensureStyleAttrs(node, stylePath, kind) {
|
|
250
|
+
if (!node || !node.openingElement || !stylePath || !kind) return;
|
|
251
|
+
if (hasJsxAttribute(node, 'data-preview-style-target')) return;
|
|
252
|
+
node.openingElement.attributes.push(...jsxStyleAttrs(stylePath, kind));
|
|
253
|
+
}
|
|
254
|
+
|
|
241
255
|
function hasDirective(ast, value) {
|
|
242
256
|
const program = ast.program || ast;
|
|
243
257
|
const first = program.body && program.body[0];
|
|
@@ -310,6 +324,8 @@ module.exports = {
|
|
|
310
324
|
siteDataBinding,
|
|
311
325
|
siteDataListBinding,
|
|
312
326
|
jsxPreviewAttr,
|
|
327
|
+
jsxStyleAttrs,
|
|
328
|
+
ensureStyleAttrs,
|
|
313
329
|
jsxStaticAttr,
|
|
314
330
|
jsxTemplatePathAttr,
|
|
315
331
|
wrapTextInEditableSpan,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function tryCoreFonts() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@deneb-ui/core');
|
|
6
|
+
} catch {
|
|
7
|
+
try {
|
|
8
|
+
return require('../../../../packages/deneb-core/dist/index.js');
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const core = tryCoreFonts();
|
|
16
|
+
|
|
17
|
+
function collectFontIdsFromSiteData(siteData) {
|
|
18
|
+
if (core && typeof core.collectFontIdsFromSiteData === 'function') {
|
|
19
|
+
const ids = core.collectFontIdsFromSiteData(siteData);
|
|
20
|
+
if (ids && ids.length) return ids;
|
|
21
|
+
}
|
|
22
|
+
const defaults = (core && core.DEFAULT_PROJECT_FONT_IDS) || ['inter', 'plus-jakarta-sans', 'playfair-display'];
|
|
23
|
+
return [...defaults];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyFontTheme(siteData, fontIds) {
|
|
27
|
+
const next = siteData && typeof siteData === 'object' ? siteData : {};
|
|
28
|
+
const theme = { ...(next.theme || {}) };
|
|
29
|
+
const structureTheme = next.template?.structure?.theme;
|
|
30
|
+
if (isPlain(structureTheme)) {
|
|
31
|
+
Object.assign(theme, structureTheme);
|
|
32
|
+
}
|
|
33
|
+
if (!theme.headingFont) {
|
|
34
|
+
theme.headingFont = prettyFont(fontIds[1] || fontIds[0] || 'inter');
|
|
35
|
+
}
|
|
36
|
+
if (!theme.bodyFont) {
|
|
37
|
+
theme.bodyFont = prettyFont(fontIds[0] || 'inter');
|
|
38
|
+
}
|
|
39
|
+
next.theme = theme;
|
|
40
|
+
if (next.template && next.template.structure) {
|
|
41
|
+
next.template.structure.theme = {
|
|
42
|
+
...(next.template.structure.theme || {}),
|
|
43
|
+
headingFont: theme.headingFont,
|
|
44
|
+
bodyFont: theme.bodyFont,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function prettyFont(id) {
|
|
51
|
+
if (core && typeof core.lookupFontDefinition === 'function') {
|
|
52
|
+
const def = core.lookupFontDefinition(id);
|
|
53
|
+
if (def?.label) return def.label;
|
|
54
|
+
}
|
|
55
|
+
return String(id || 'Inter');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isPlain(value) {
|
|
59
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
collectFontIdsFromSiteData,
|
|
64
|
+
applyFontTheme,
|
|
65
|
+
};
|
package/src/arc/index.cjs
CHANGED
|
@@ -477,11 +477,7 @@ function runDenebArc(projectDir, projectName, options = {}) {
|
|
|
477
477
|
} else {
|
|
478
478
|
console.log(' \x1b[32m✓\x1b[0m Fivora strict contract');
|
|
479
479
|
}
|
|
480
|
-
|
|
481
|
-
console.log(
|
|
482
|
-
` \x1b[33m⚠\x1b[0m ${fivoraAudit.uncoveredVisibleText.length} visible text node(s) still uncovered — run \x1b[1mdeneb validate .\x1b[0m before packaging`
|
|
483
|
-
);
|
|
484
|
-
}
|
|
480
|
+
printer.printUncoveredText(fivoraAudit.uncoveredVisibleText);
|
|
485
481
|
coverage.actionLinkContracts = contracts.fieldPaths.filter((p) => /Url$/.test(p)).length;
|
|
486
482
|
coverage.contractCollisions = contracts.actionCollisions;
|
|
487
483
|
console.log(` Action/link contracts validated: ${coverage.actionLinkContracts}`);
|
|
@@ -560,7 +556,10 @@ function runDenebArc(projectDir, projectName, options = {}) {
|
|
|
560
556
|
writeJson(path.join(journalDir, 'validation.json'), validation);
|
|
561
557
|
writeJson(path.join(projectDir, '.deneb', 'report.json'), report);
|
|
562
558
|
|
|
563
|
-
if (outcome === 'success')
|
|
559
|
+
if (outcome === 'success') {
|
|
560
|
+
printer.printSuccess();
|
|
561
|
+
printer.printDeveloperNextSteps();
|
|
562
|
+
}
|
|
564
563
|
return result;
|
|
565
564
|
}
|
|
566
565
|
|
package/src/arc/learning.cjs
CHANGED
|
@@ -85,6 +85,29 @@ function recordExperience({ projectDir, profile, plan, validation, outcome, tele
|
|
|
85
85
|
return records;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function loadFingerprintBoost(fingerprint) {
|
|
89
|
+
if (!fingerprint) {
|
|
90
|
+
return { boost: 0, skip: false, state: null };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const store = readJsonSafe(fingerprintStorePath(), { fingerprints: {} }) || { fingerprints: {} };
|
|
94
|
+
const entry = store.fingerprints?.[fingerprint];
|
|
95
|
+
if (!entry) return { boost: 0, skip: false, state: null };
|
|
96
|
+
if (entry.state === 'deprecated') {
|
|
97
|
+
return { boost: 0, skip: true, state: 'deprecated' };
|
|
98
|
+
}
|
|
99
|
+
if (entry.state === 'verified') {
|
|
100
|
+
return { boost: 0.08, skip: false, state: 'verified' };
|
|
101
|
+
}
|
|
102
|
+
if (entry.state === 'candidate') {
|
|
103
|
+
return { boost: 0.03, skip: false, state: 'candidate' };
|
|
104
|
+
}
|
|
105
|
+
return { boost: 0, skip: false, state: entry.state || 'observed' };
|
|
106
|
+
} catch {
|
|
107
|
+
return { boost: 0, skip: false, state: null };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
function mapOperation(operation) {
|
|
89
112
|
switch (operation) {
|
|
90
113
|
case 'extract-text':
|
|
@@ -96,6 +119,8 @@ function mapOperation(operation) {
|
|
|
96
119
|
return 'url-extraction';
|
|
97
120
|
case 'split-action-contract':
|
|
98
121
|
return 'contract-split';
|
|
122
|
+
case 'style-bind':
|
|
123
|
+
return 'style-bind';
|
|
99
124
|
case 'collection-conversion':
|
|
100
125
|
return 'collection-conversion';
|
|
101
126
|
default:
|
|
@@ -176,6 +201,7 @@ function redactSecrets(value) {
|
|
|
176
201
|
|
|
177
202
|
module.exports = {
|
|
178
203
|
recordExperience,
|
|
204
|
+
loadFingerprintBoost,
|
|
179
205
|
registryArchitecture,
|
|
180
206
|
redactSecrets,
|
|
181
207
|
promoteState,
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { readJsonSafe, writeJson, deepMerge, isPlainObject } = require('./fs-utils.cjs');
|
|
5
5
|
const { ARC_VERSION, SCHEMA_VERSION } = require('./version.cjs');
|
|
6
|
+
const { collectFontIdsFromSiteData, applyFontTheme } = require('./font-plan.cjs');
|
|
6
7
|
const { humanLabel, classifyFieldType } = require('./field-paths.cjs');
|
|
7
8
|
const {
|
|
8
9
|
enumerateContentPaths,
|
|
@@ -154,6 +155,18 @@ function collectFieldsFromPlan(plan) {
|
|
|
154
155
|
return fields;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
function collectStylesFromPlan(plan, existingStyles) {
|
|
159
|
+
const styles = isPlainObject(existingStyles) ? { ...existingStyles } : {};
|
|
160
|
+
for (const file of plan.files || []) {
|
|
161
|
+
for (const t of file.transformations || []) {
|
|
162
|
+
if (t.operation !== 'style-bind' || !t.stylePath) continue;
|
|
163
|
+
if (t.stylePath.includes('[*]')) continue;
|
|
164
|
+
if (!styles[t.stylePath]) styles[t.stylePath] = {};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return styles;
|
|
168
|
+
}
|
|
169
|
+
|
|
157
170
|
function baseContent(projectName, routes) {
|
|
158
171
|
const navLabels = {};
|
|
159
172
|
for (const page of routes) navLabels[page.id] = page.label || page.id;
|
|
@@ -339,8 +352,12 @@ function buildSiteDataAndManifest({
|
|
|
339
352
|
requiredFeatures: [],
|
|
340
353
|
},
|
|
341
354
|
content,
|
|
355
|
+
styles: collectStylesFromPlan(plan, existingSiteData?.styles),
|
|
356
|
+
...(isPlainObject(existingSiteData?.theme) ? { theme: { ...existingSiteData.theme } } : {}),
|
|
342
357
|
};
|
|
343
358
|
|
|
359
|
+
applyFontTheme(siteData, collectFontIdsFromSiteData(siteData));
|
|
360
|
+
|
|
344
361
|
if (!siteData.template.structure) siteData.template.structure = {};
|
|
345
362
|
siteData.template.structure.pages = routes.map((p) => p.id);
|
|
346
363
|
|
package/src/arc/planner.cjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const { CONFIDENCE } = require('./version.cjs');
|
|
4
4
|
const { inferSection, inferFieldName, buildFieldPath, classifyFieldType, uniquePath } = require('./field-paths.cjs');
|
|
5
5
|
const { classifyHref } = require('./adapters.cjs');
|
|
6
|
+
const { loadFingerprintBoost } = require('./learning.cjs');
|
|
7
|
+
const { appendStyleBindTransforms } = require('./style-candidates.cjs');
|
|
6
8
|
|
|
7
9
|
function recipeBoost(candidate, recipe) {
|
|
8
10
|
if (!recipe) return 0;
|
|
@@ -30,7 +32,21 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
30
32
|
for (const analysis of analyses) {
|
|
31
33
|
const transformations = [];
|
|
32
34
|
for (const candidate of analysis.candidates || []) {
|
|
33
|
-
const
|
|
35
|
+
const fingerprintHint = loadFingerprintBoost(candidate.fingerprint);
|
|
36
|
+
if (fingerprintHint.skip) {
|
|
37
|
+
skipped.push({
|
|
38
|
+
file: candidate.file,
|
|
39
|
+
loc: candidate.loc,
|
|
40
|
+
reason: 'fingerprint-deprecated',
|
|
41
|
+
confidence: candidate.confidence || 0,
|
|
42
|
+
kind: candidate.kind,
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const confidence = Math.min(
|
|
47
|
+
0.99,
|
|
48
|
+
(candidate.confidence || 0) + recipeBoost(candidate, recipe) + (fingerprintHint.boost || 0),
|
|
49
|
+
);
|
|
34
50
|
const decision = decideThreshold(confidence, candidate);
|
|
35
51
|
const section = inferSection({
|
|
36
52
|
componentName: candidate.componentName,
|
|
@@ -75,6 +91,8 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
75
91
|
why: candidate.reason,
|
|
76
92
|
recipe: recipe?.name || null,
|
|
77
93
|
confidence,
|
|
94
|
+
fingerprintBoost: fingerprintHint.boost || 0,
|
|
95
|
+
fingerprintState: fingerprintHint.state || null,
|
|
78
96
|
},
|
|
79
97
|
};
|
|
80
98
|
|
|
@@ -188,6 +206,8 @@ function planTransformations({ profile, analyses, recipe }) {
|
|
|
188
206
|
});
|
|
189
207
|
}
|
|
190
208
|
|
|
209
|
+
appendStyleBindTransforms(filePlans);
|
|
210
|
+
|
|
191
211
|
return {
|
|
192
212
|
files: filePlans,
|
|
193
213
|
skipped,
|
|
@@ -212,12 +232,17 @@ function summarizePlan(filePlans, skipped) {
|
|
|
212
232
|
const auto = filePlans.reduce((n, f) => n + f.transformations.filter((t) => t.decision === 'auto').length, 0);
|
|
213
233
|
const validate = filePlans.reduce((n, f) => n + f.transformations.filter((t) => t.decision === 'validate').length, 0);
|
|
214
234
|
const filesAffected = filePlans.filter((f) => f.transformations.length > 0).length;
|
|
235
|
+
const styleBinds = filePlans.reduce(
|
|
236
|
+
(n, f) => n + f.transformations.filter((t) => t.operation === 'style-bind').length,
|
|
237
|
+
0,
|
|
238
|
+
);
|
|
215
239
|
return {
|
|
216
240
|
planned,
|
|
217
241
|
auto,
|
|
218
242
|
validate,
|
|
219
243
|
skipped: skipped.length,
|
|
220
244
|
filesAffected,
|
|
245
|
+
styleBinds,
|
|
221
246
|
};
|
|
222
247
|
}
|
|
223
248
|
|
package/src/arc/printer.cjs
CHANGED
|
@@ -67,6 +67,7 @@ function printPlan(plan) {
|
|
|
67
67
|
if (plan.stats.validate) info(`${plan.stats.validate} transformations queued with extra validation`);
|
|
68
68
|
const skippedDynamic = plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length;
|
|
69
69
|
const skippedLow = plan.skipped.filter((s) => (s.confidence || 1) < 0.6).length;
|
|
70
|
+
if (plan.stats.styleBinds) ok(`${plan.stats.styleBinds} Fivora style-bind contracts`);
|
|
70
71
|
if (skippedDynamic) ok(`${skippedDynamic} existing dynamic values preserved`);
|
|
71
72
|
if (skippedLow) warn(`${skippedLow} low-confidence candidates skipped`);
|
|
72
73
|
}
|
|
@@ -96,8 +97,12 @@ function printExplain(plan) {
|
|
|
96
97
|
if (!file.transformations.length) continue;
|
|
97
98
|
console.log(` ${C.bold}${file.file}${C.reset}`);
|
|
98
99
|
for (const t of file.transformations) {
|
|
99
|
-
|
|
100
|
-
console.log(`
|
|
100
|
+
const target = t.stylePath || t.field || t.urlField || t.listField || '';
|
|
101
|
+
console.log(` - ${t.operation} ${target}`);
|
|
102
|
+
const boost = t.explain?.fingerprintBoost
|
|
103
|
+
? ` fingerprint: ${t.explain.fingerprintState}+${t.explain.fingerprintBoost}`
|
|
104
|
+
: '';
|
|
105
|
+
console.log(` ${C.dim}why: ${t.reason} confidence: ${t.confidence} recipe: ${t.recipeId || 'none'}${boost}${C.reset}`);
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -130,6 +135,29 @@ function printSuccess() {
|
|
|
130
135
|
console.log(`\n${C.green}${C.bold}Deneb ARC completed successfully.${C.reset}\n`);
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
function printUncoveredText(findings = []) {
|
|
139
|
+
if (!findings.length) return;
|
|
140
|
+
warn(`${findings.length} visible text node(s) still uncovered — run deneb validate . before packaging`);
|
|
141
|
+
for (const item of findings.slice(0, 12)) {
|
|
142
|
+
const loc = item.filePath ? `${item.filePath}${item.line ? `:${item.line}` : ''}` : '';
|
|
143
|
+
const snippet = String(item.text || '').slice(0, 80);
|
|
144
|
+
console.log(` ${C.dim}${loc} <${item.tag}> ${snippet}${C.reset}`);
|
|
145
|
+
}
|
|
146
|
+
if (findings.length > 12) {
|
|
147
|
+
console.log(` ${C.dim}... ${findings.length - 12} more in .deneb/report.json${C.reset}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printDeveloperNextSteps() {
|
|
152
|
+
heading('Next steps for Fivora');
|
|
153
|
+
console.log(` ${C.cyan}1.${C.reset} npm run lab ${C.dim}preview visual editing locally${C.reset}`);
|
|
154
|
+
console.log(` ${C.cyan}2.${C.reset} deneb fonts install . ${C.dim}self-host Google Fonts used by this site${C.reset}`);
|
|
155
|
+
console.log(` ${C.cyan}3.${C.reset} npm run validate ${C.dim}same contract Fivora ingest uses${C.reset}`);
|
|
156
|
+
console.log(` ${C.cyan}4.${C.reset} npm run validate-and-zip ${C.dim}upload-ready ZIP${C.reset}`);
|
|
157
|
+
console.log(` ${C.dim}Style edits: Fivora sends FIVORA_PREVIEW_STYLE_PATCH to data-preview-style-target nodes.${C.reset}`);
|
|
158
|
+
console.log(` ${C.dim}Re-run with --explain to see why each field/style was bound.${C.reset}\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
function printRollback(reason) {
|
|
134
162
|
console.log(`\n${C.yellow}Deneb ARC rolled back the conversion.${C.reset}`);
|
|
135
163
|
console.log(`${C.dim}${reason}${C.reset}\n`);
|
|
@@ -146,6 +174,8 @@ module.exports = {
|
|
|
146
174
|
printDryRun,
|
|
147
175
|
printError,
|
|
148
176
|
printSuccess,
|
|
177
|
+
printUncoveredText,
|
|
178
|
+
printDeveloperNextSteps,
|
|
149
179
|
printRollback,
|
|
150
180
|
ok,
|
|
151
181
|
warn,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TEXT_OPS = new Set(['extract-text', 'wrap-text-span']);
|
|
4
|
+
const BUTTON_TAGS = new Set(['button', 'Button', 'CTAButton']);
|
|
5
|
+
|
|
6
|
+
function inferStyleKind(transform) {
|
|
7
|
+
if (transform.operation === 'collection-conversion') return 'grid';
|
|
8
|
+
if (transform.operation === 'split-action-contract') return 'text';
|
|
9
|
+
if (BUTTON_TAGS.has(transform.tag) && transform.operation === 'extract-text') return 'button';
|
|
10
|
+
if (TEXT_OPS.has(transform.operation)) return 'text';
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stylePathFor(transform, kind) {
|
|
15
|
+
if (kind === 'grid' && transform.listField) return `${transform.listField}.grid`;
|
|
16
|
+
if (kind === 'card' && transform.listField) return `${transform.listField}[*].card`;
|
|
17
|
+
if (transform.operation === 'split-action-contract') return transform.labelField;
|
|
18
|
+
return transform.field || transform.labelField || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds explainable style-bind operations next to content transforms so
|
|
23
|
+
* developers see why Fivora can restyle a node without CSS rewrites.
|
|
24
|
+
*/
|
|
25
|
+
function appendStyleBindTransforms(filePlans) {
|
|
26
|
+
for (const file of filePlans || []) {
|
|
27
|
+
const extra = [];
|
|
28
|
+
for (const transform of file.transformations || []) {
|
|
29
|
+
if (transform.operation === 'style-bind') continue;
|
|
30
|
+
if (transform.operation === 'wrap-text-span') continue;
|
|
31
|
+
if (transform.operation === 'collection-conversion' && transform.listField) {
|
|
32
|
+
extra.push(makeStyleBind(transform, `${transform.listField}.grid`, 'grid'));
|
|
33
|
+
extra.push(makeStyleBind(transform, `${transform.listField}[*].card`, 'card'));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const kind = inferStyleKind(transform);
|
|
37
|
+
const stylePath = stylePathFor(transform, kind);
|
|
38
|
+
if (!kind || !stylePath) continue;
|
|
39
|
+
extra.push(makeStyleBind(transform, stylePath, kind));
|
|
40
|
+
}
|
|
41
|
+
file.transformations = [...(file.transformations || []), ...extra];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeStyleBind(transform, stylePath, kind) {
|
|
46
|
+
return {
|
|
47
|
+
loc: transform.loc,
|
|
48
|
+
operation: 'style-bind',
|
|
49
|
+
tag: transform.tag,
|
|
50
|
+
file: transform.file,
|
|
51
|
+
confidence: Math.min(0.99, (transform.confidence || 0.9) - 0.02),
|
|
52
|
+
decision: transform.decision === 'skip' ? 'skip' : 'auto',
|
|
53
|
+
reason: `Bind Fivora style contract (${kind}) without rewriting CSS`,
|
|
54
|
+
field: transform.field,
|
|
55
|
+
stylePath,
|
|
56
|
+
styleKind: kind,
|
|
57
|
+
fingerprint: transform.fingerprint,
|
|
58
|
+
recipeId: transform.recipeId || null,
|
|
59
|
+
explain: {
|
|
60
|
+
detected: `${transform.tag} style-${kind}`,
|
|
61
|
+
why: 'Fivora visual editor patches --deneb-* CSS variables via FIVORA_PREVIEW_STYLE_PATCH',
|
|
62
|
+
recipe: transform.recipeId || null,
|
|
63
|
+
confidence: transform.confidence,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
appendStyleBindTransforms,
|
|
70
|
+
inferStyleKind,
|
|
71
|
+
stylePathFor,
|
|
72
|
+
};
|