@deneb-ui/cli 2.0.31 → 2.0.33
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 +108 -1
- package/src/arc/ast.cjs +139 -1
- package/src/arc/field-paths.cjs +10 -0
- package/src/arc/index.cjs +20 -2
- package/src/arc/manifest.cjs +70 -2
- package/src/arc/transformer.cjs +188 -46
- package/src/tools/template-converter.cjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.33",
|
|
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.33",
|
|
53
53
|
"adm-zip": "^0.6.0",
|
|
54
54
|
"recast": "^0.23.11"
|
|
55
55
|
},
|
|
@@ -10,11 +10,57 @@ const { scanProject, buildDependencyGraph } = require('../scanner.cjs');
|
|
|
10
10
|
const { analyzeFile } = require('../semantic.cjs');
|
|
11
11
|
const { planTransformations } = require('../planner.cjs');
|
|
12
12
|
const { applyFilePlan } = require('../transformer.cjs');
|
|
13
|
-
const { buildFieldPath, inferFieldName } = require('../field-paths.cjs');
|
|
13
|
+
const { buildFieldPath, inferFieldName, isListActionCtaKey } = require('../field-paths.cjs');
|
|
14
|
+
const { enrichSchemasFromContent } = require('../manifest.cjs');
|
|
14
15
|
const { runDenebArc } = require('../index.cjs');
|
|
15
16
|
const { parseSource } = require('../ast.cjs');
|
|
16
17
|
const { loadFingerprintBoost } = require('../learning.cjs');
|
|
17
18
|
|
|
19
|
+
test('field-paths recognizes list action CTA keys', () => {
|
|
20
|
+
assert.equal(isListActionCtaKey('preOrderCta'), true);
|
|
21
|
+
assert.equal(isListActionCtaKey('addToTrayCta'), true);
|
|
22
|
+
assert.equal(isListActionCtaKey('features'), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('manifest enrichSchemasFromContent registers list CTA and paired directions fields', () => {
|
|
26
|
+
const sections = [
|
|
27
|
+
{
|
|
28
|
+
id: 'contact',
|
|
29
|
+
path: 'contact',
|
|
30
|
+
type: 'object',
|
|
31
|
+
label: 'Contact',
|
|
32
|
+
fields: [],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'home',
|
|
36
|
+
path: 'home',
|
|
37
|
+
type: 'object',
|
|
38
|
+
label: 'Home',
|
|
39
|
+
fields: [],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
enrichSchemasFromContent(
|
|
43
|
+
{
|
|
44
|
+
contact: {
|
|
45
|
+
directionsLabel: 'Get Directions',
|
|
46
|
+
directionsUrl: 'https://maps.example',
|
|
47
|
+
},
|
|
48
|
+
home: {
|
|
49
|
+
demoPreOrderCta: [{ buttonLabel: 'Order', buttonUrl: 'https://wa.me/123' }],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
sections
|
|
53
|
+
);
|
|
54
|
+
const contactFields = sections.find((s) => s.id === 'contact').fields;
|
|
55
|
+
assert.ok(contactFields.some((f) => f.key === 'directionsUrl' && f.type === 'url'));
|
|
56
|
+
assert.ok(contactFields.some((f) => f.key === 'directionsLabel'));
|
|
57
|
+
const homeFields = sections.find((s) => s.id === 'home').fields;
|
|
58
|
+
const list = homeFields.find((f) => f.key === 'demoPreOrderCta');
|
|
59
|
+
assert.ok(list && list.type === 'list');
|
|
60
|
+
assert.ok(list.fields.some((f) => f.key === 'buttonLabel'));
|
|
61
|
+
assert.ok(list.fields.some((f) => f.key === 'buttonUrl' && f.type === 'url'));
|
|
62
|
+
});
|
|
63
|
+
|
|
18
64
|
test('unseen fingerprints do not change planner confidence', () => {
|
|
19
65
|
const hint = loadFingerprintBoost(null);
|
|
20
66
|
assert.equal(hint.boost, 0);
|
|
@@ -109,6 +155,8 @@ test('AST transformer preserves className and uses nullish fallbacks', () => {
|
|
|
109
155
|
assert.match(result.code, /data-preview-field-path=/);
|
|
110
156
|
assert.match(result.code, /\?\?/);
|
|
111
157
|
assert.match(result.code, /<span[\s\S]*data-preview-field-path="/);
|
|
158
|
+
assert.match(result.code, /<span[^>]*hidden[^>]*data-preview-field-path="/);
|
|
159
|
+
assert.match(result.code, /data-preview-static="action-link"/);
|
|
112
160
|
assert.doesNotMatch(result.code, /'use client'/);
|
|
113
161
|
assert.match(result.code, /site-data\.json|@\/data\/site-data\.json/);
|
|
114
162
|
assert.match(result.code, /data-preview-style-target=/);
|
|
@@ -519,3 +567,62 @@ test('conflicting data-preview-static is stripped when element has editable mark
|
|
|
519
567
|
const placementErrors = errors.filter((e) => e.includes('cannot share an element with data-preview-static'));
|
|
520
568
|
assert.equal(placementErrors.length, 0);
|
|
521
569
|
});
|
|
570
|
+
|
|
571
|
+
test('sanitizeDuplicateBindings keeps a single useSiteData import', () => {
|
|
572
|
+
const { parseSource, printSource, sanitizeDuplicateBindings } = require('../ast.cjs');
|
|
573
|
+
const code = `'use client';
|
|
574
|
+
import { useSiteData, contentText } from '@/lib/siteDataContext';
|
|
575
|
+
import { useSiteData, contentObject } from '@deneb-ui/ui';
|
|
576
|
+
export function Shop() {
|
|
577
|
+
const siteData = useSiteData();
|
|
578
|
+
return <div>{contentText(contentObject(siteData).title)}</div>;
|
|
579
|
+
}
|
|
580
|
+
`;
|
|
581
|
+
const ast = parseSource(code, 'shop.tsx');
|
|
582
|
+
sanitizeDuplicateBindings(ast);
|
|
583
|
+
const out = printSource(ast, code);
|
|
584
|
+
const importUses = [...out.matchAll(/import\s*\{([^}]+)\}\s*from/g)].flatMap((m) =>
|
|
585
|
+
m[1].split(',').map((s) => s.trim()).filter((s) => s === 'useSiteData')
|
|
586
|
+
);
|
|
587
|
+
assert.equal(importUses.length, 1);
|
|
588
|
+
assert.match(out, /siteDataContext/);
|
|
589
|
+
assert.doesNotMatch(out, /import\s*\{[^}]*useSiteData[^}]*\}\s*from\s*['"]@deneb-ui\/ui['"]/);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test('recursive SiteDataProvider wrappers are flattened to a package re-export', () => {
|
|
593
|
+
const { rewriteRecursiveSiteDataContext } = require('../transformer.cjs');
|
|
594
|
+
const code = `'use client';
|
|
595
|
+
import { SiteDataProvider as BaseSiteDataProvider } from '@deneb-ui/ui';
|
|
596
|
+
import initialSiteData from '@/data/site-data.json';
|
|
597
|
+
export function SiteDataProvider({ children, ...props }) {
|
|
598
|
+
return (
|
|
599
|
+
<BaseSiteDataProvider initialSiteData={initialSiteData} {...props}>
|
|
600
|
+
{children}
|
|
601
|
+
</BaseSiteDataProvider>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
`;
|
|
605
|
+
const out = rewriteRecursiveSiteDataContext(code);
|
|
606
|
+
assert.equal(out.updated, true);
|
|
607
|
+
assert.match(out.code, /export \{/);
|
|
608
|
+
assert.match(out.code, /SiteDataProvider/);
|
|
609
|
+
assert.match(out.code, /from '@deneb-ui\/ui'/);
|
|
610
|
+
assert.doesNotMatch(out.code, /export function SiteDataProvider/);
|
|
611
|
+
assert.doesNotMatch(out.code, /BaseSiteDataProvider/);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('import plus export of useSiteData is rewritten to export-from', () => {
|
|
615
|
+
const { parseSource, printSource, sanitizeDuplicateBindings } = require('../ast.cjs');
|
|
616
|
+
const code = `'use client';
|
|
617
|
+
import { SiteDataProvider as BaseSiteDataProvider, useSiteData, contentText } from '@deneb-ui/ui';
|
|
618
|
+
export function SiteDataProvider({ children }) {
|
|
619
|
+
return <BaseSiteDataProvider>{children}</BaseSiteDataProvider>;
|
|
620
|
+
}
|
|
621
|
+
export { useSiteData, contentText };
|
|
622
|
+
`;
|
|
623
|
+
const ast = parseSource(code, 'siteDataContext.tsx');
|
|
624
|
+
sanitizeDuplicateBindings(ast);
|
|
625
|
+
const out = printSource(ast, code);
|
|
626
|
+
assert.match(out, /export\s*\{[^}]*useSiteData[^}]*\}\s*from\s*['"]@deneb-ui\/ui['"]/);
|
|
627
|
+
assert.doesNotMatch(out, /import\s*\{[^}]*useSiteData/);
|
|
628
|
+
});
|
package/src/arc/ast.cjs
CHANGED
|
@@ -264,11 +264,144 @@ function hasDirective(ast, value) {
|
|
|
264
264
|
return false;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function localNameOfSpecifier(spec) {
|
|
268
|
+
return spec?.local?.name || spec?.imported?.name || spec?.exported?.name || null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function dedupeImportSpecifiers(ast) {
|
|
272
|
+
const program = ast.program || ast;
|
|
273
|
+
const seenLocals = new Set();
|
|
274
|
+
for (const node of program.body || []) {
|
|
275
|
+
if (node.type !== 'ImportDeclaration' || !Array.isArray(node.specifiers)) continue;
|
|
276
|
+
node.specifiers = node.specifiers.filter((spec) => {
|
|
277
|
+
const local = localNameOfSpecifier(spec);
|
|
278
|
+
if (!local) return true;
|
|
279
|
+
if (seenLocals.has(local)) return false;
|
|
280
|
+
seenLocals.add(local);
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function preferLocalSiteDataImport(ast) {
|
|
287
|
+
const program = ast.program || ast;
|
|
288
|
+
const body = program.body || [];
|
|
289
|
+
const localNames = new Set();
|
|
290
|
+
for (const node of body) {
|
|
291
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
292
|
+
const src = node.source && node.source.value;
|
|
293
|
+
if (typeof src !== 'string' || !/siteDataContext/.test(src)) continue;
|
|
294
|
+
for (const spec of node.specifiers || []) {
|
|
295
|
+
const local = localNameOfSpecifier(spec);
|
|
296
|
+
if (local) localNames.add(local);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (localNames.size === 0) return;
|
|
300
|
+
for (const node of body) {
|
|
301
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
302
|
+
const src = node.source && node.source.value;
|
|
303
|
+
if (src !== '@deneb-ui/ui' && src !== 'deneb-ui' && src !== '@fivora/editable-components') continue;
|
|
304
|
+
node.specifiers = (node.specifiers || []).filter((spec) => !localNames.has(localNameOfSpecifier(spec)));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function dropImportedNameIfLocallyDeclared(ast) {
|
|
309
|
+
const program = ast.program || ast;
|
|
310
|
+
const declared = new Set();
|
|
311
|
+
for (const node of program.body || []) {
|
|
312
|
+
if (node.type === 'FunctionDeclaration' && node.id && node.id.name) {
|
|
313
|
+
declared.add(node.id.name);
|
|
314
|
+
}
|
|
315
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
316
|
+
const decl = node.declaration;
|
|
317
|
+
if (decl.type === 'FunctionDeclaration' && decl.id && decl.id.name) {
|
|
318
|
+
declared.add(decl.id.name);
|
|
319
|
+
}
|
|
320
|
+
if (decl.type === 'VariableDeclaration') {
|
|
321
|
+
for (const d of decl.declarations || []) {
|
|
322
|
+
if (d.id && d.id.type === 'Identifier') declared.add(d.id.name);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (declared.size === 0) return;
|
|
328
|
+
for (const node of program.body || []) {
|
|
329
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
330
|
+
node.specifiers = (node.specifiers || []).filter((spec) => !declared.has(localNameOfSpecifier(spec)));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function rewriteImportedReexports(ast) {
|
|
335
|
+
const program = ast.program || ast;
|
|
336
|
+
const importSourceByLocal = new Map();
|
|
337
|
+
for (const node of program.body || []) {
|
|
338
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
339
|
+
const src = node.source && node.source.value;
|
|
340
|
+
for (const spec of node.specifiers || []) {
|
|
341
|
+
const local = localNameOfSpecifier(spec);
|
|
342
|
+
if (local && src) importSourceByLocal.set(local, src);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const exportDecls = (program.body || []).filter(
|
|
347
|
+
(node) => node.type === 'ExportNamedDeclaration' && !node.source && node.specifiers && node.specifiers.length
|
|
348
|
+
);
|
|
349
|
+
for (const node of exportDecls) {
|
|
350
|
+
const groups = new Map();
|
|
351
|
+
const keep = [];
|
|
352
|
+
for (const spec of node.specifiers) {
|
|
353
|
+
const local = spec.local?.name;
|
|
354
|
+
const src = local && importSourceByLocal.get(local);
|
|
355
|
+
if (!src) {
|
|
356
|
+
keep.push(spec);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!groups.has(src)) groups.set(src, []);
|
|
360
|
+
groups.get(src).push(spec);
|
|
361
|
+
}
|
|
362
|
+
if (!groups.size) continue;
|
|
363
|
+
node.specifiers = keep;
|
|
364
|
+
let insertAt = program.body.indexOf(node) + 1;
|
|
365
|
+
for (const [src, specs] of groups.entries()) {
|
|
366
|
+
program.body.splice(insertAt, 0, b.exportNamedDeclaration(null, specs, b.stringLiteral(src)));
|
|
367
|
+
insertAt++;
|
|
368
|
+
for (const spec of specs) {
|
|
369
|
+
const local = spec.local?.name;
|
|
370
|
+
for (const imp of program.body) {
|
|
371
|
+
if (imp.type !== 'ImportDeclaration') continue;
|
|
372
|
+
if (!imp.specifiers) continue;
|
|
373
|
+
imp.specifiers = imp.specifiers.filter((s) => localNameOfSpecifier(s) !== local);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function stripEmptyImportAndExportDecls(ast) {
|
|
381
|
+
const program = ast.program || ast;
|
|
382
|
+
program.body = (program.body || []).filter((node) => {
|
|
383
|
+
if (node.type === 'ImportDeclaration') {
|
|
384
|
+
return (node.specifiers || []).length > 0;
|
|
385
|
+
}
|
|
386
|
+
if (node.type === 'ExportNamedDeclaration' && !node.declaration && !node.source) {
|
|
387
|
+
return (node.specifiers || []).length > 0;
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function sanitizeDuplicateBindings(ast) {
|
|
394
|
+
dropImportedNameIfLocallyDeclared(ast);
|
|
395
|
+
preferLocalSiteDataImport(ast);
|
|
396
|
+
rewriteImportedReexports(ast);
|
|
397
|
+
dedupeImportSpecifiers(ast);
|
|
398
|
+
stripEmptyImportAndExportDecls(ast);
|
|
399
|
+
}
|
|
400
|
+
|
|
267
401
|
function ensureImport(ast, source, names) {
|
|
268
402
|
const program = ast.program || ast;
|
|
269
403
|
const body = program.body || [];
|
|
270
404
|
|
|
271
|
-
// Deduplicate against any import in the entire module so we never import the same identifier twice
|
|
272
405
|
const alreadyImported = new Set();
|
|
273
406
|
for (const node of body) {
|
|
274
407
|
if (node.type === 'ImportDeclaration' && Array.isArray(node.specifiers)) {
|
|
@@ -277,6 +410,10 @@ function ensureImport(ast, source, names) {
|
|
|
277
410
|
if (local) alreadyImported.add(local);
|
|
278
411
|
}
|
|
279
412
|
}
|
|
413
|
+
if (node.type === 'FunctionDeclaration' && node.id?.name) alreadyImported.add(node.id.name);
|
|
414
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'FunctionDeclaration' && node.declaration.id?.name) {
|
|
415
|
+
alreadyImported.add(node.declaration.id.name);
|
|
416
|
+
}
|
|
280
417
|
}
|
|
281
418
|
|
|
282
419
|
const namesToImport = names.filter((name) => !alreadyImported.has(name));
|
|
@@ -340,6 +477,7 @@ module.exports = {
|
|
|
340
477
|
hasDirective,
|
|
341
478
|
ensureImport,
|
|
342
479
|
ensureDefaultImport,
|
|
480
|
+
sanitizeDuplicateBindings,
|
|
343
481
|
codeHasIdentifier,
|
|
344
482
|
t,
|
|
345
483
|
b,
|
package/src/arc/field-paths.cjs
CHANGED
|
@@ -141,6 +141,14 @@ function humanLabel(fieldPath) {
|
|
|
141
141
|
.trim();
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function isListActionCtaKey(key) {
|
|
145
|
+
return /Cta$/i.test(String(key || ''));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listActionCtaItemFieldNames() {
|
|
149
|
+
return ['buttonLabel', 'buttonUrl'];
|
|
150
|
+
}
|
|
151
|
+
|
|
144
152
|
function classifyFieldType(kind, value) {
|
|
145
153
|
if (kind === 'image') return 'image';
|
|
146
154
|
if (kind === 'url') return 'url';
|
|
@@ -162,4 +170,6 @@ module.exports = {
|
|
|
162
170
|
buildFieldPath,
|
|
163
171
|
humanLabel,
|
|
164
172
|
classifyFieldType,
|
|
173
|
+
isListActionCtaKey,
|
|
174
|
+
listActionCtaItemFieldNames,
|
|
165
175
|
};
|
package/src/arc/index.cjs
CHANGED
|
@@ -17,7 +17,7 @@ const { walkFiles, isJsxFile, rel, copyFilePreserve, writeJson, readJsonSafe, fi
|
|
|
17
17
|
const { scanProject, buildDependencyGraph, inferOwnerScope } = require('./scanner.cjs');
|
|
18
18
|
const { analyzeFile, collectDesignSnapshot } = require('./semantic.cjs');
|
|
19
19
|
const { planTransformations } = require('./planner.cjs');
|
|
20
|
-
const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, ensureJsonModule, sanitizeContradictoryMarkersInSource } = require('./transformer.cjs');
|
|
20
|
+
const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, resolveSiteDataRuntimeSpecifier, rewriteRecursiveSiteDataContext, ensureJsonModule, sanitizeContradictoryMarkersInSource } = require('./transformer.cjs');
|
|
21
21
|
const { parseSource } = require('./ast.cjs');
|
|
22
22
|
const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
|
|
23
23
|
const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
|
|
@@ -306,8 +306,9 @@ function runDenebArc(projectDir, projectName, options = {}) {
|
|
|
306
306
|
backupFile(projectDir, backupDir, layoutFile);
|
|
307
307
|
const layoutRel = rel(projectDir, layoutFile);
|
|
308
308
|
const siteDataImport = resolveSiteDataSpecifier(profile, layoutRel);
|
|
309
|
+
const providerImport = resolveSiteDataRuntimeSpecifier(profile);
|
|
309
310
|
const original = fs.readFileSync(layoutFile, 'utf8');
|
|
310
|
-
const instrumented = instrumentLayoutSource(original, siteDataImport);
|
|
311
|
+
const instrumented = instrumentLayoutSource(original, siteDataImport, providerImport);
|
|
311
312
|
if (instrumented.updated && instrumented.code !== original) {
|
|
312
313
|
fs.writeFileSync(layoutFile, instrumented.code, 'utf8');
|
|
313
314
|
changedFiles.push(layoutRel);
|
|
@@ -315,6 +316,23 @@ function runDenebArc(projectDir, projectName, options = {}) {
|
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
const contextCandidates = [
|
|
320
|
+
path.join(projectDir, 'src', 'lib', 'siteDataContext.tsx'),
|
|
321
|
+
path.join(projectDir, 'src', 'lib', 'siteDataContext.ts'),
|
|
322
|
+
path.join(projectDir, 'lib', 'siteDataContext.tsx'),
|
|
323
|
+
path.join(projectDir, 'lib', 'siteDataContext.ts'),
|
|
324
|
+
];
|
|
325
|
+
for (const abs of contextCandidates) {
|
|
326
|
+
if (!fs.existsSync(abs)) continue;
|
|
327
|
+
backupFile(projectDir, backupDir, abs);
|
|
328
|
+
const original = fs.readFileSync(abs, 'utf8');
|
|
329
|
+
const rewritten = rewriteRecursiveSiteDataContext(original);
|
|
330
|
+
if (rewritten.updated && rewritten.code !== original) {
|
|
331
|
+
fs.writeFileSync(abs, rewritten.code, 'utf8');
|
|
332
|
+
changedFiles.push(rel(projectDir, abs));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
318
336
|
for (const filePlan of plan.files) {
|
|
319
337
|
if (!filePlan.transformations.length || filePlan.skippedFile) continue;
|
|
320
338
|
const abs = path.join(projectDir, filePlan.file);
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -59,7 +59,7 @@ function upsertSchemaField(sections, fieldPath, fieldType, label, required = fal
|
|
|
59
59
|
if (!existing) {
|
|
60
60
|
fields.push({
|
|
61
61
|
key,
|
|
62
|
-
type: fieldType === 'url' ? '
|
|
62
|
+
type: fieldType === 'url' ? 'url' : fieldType === 'textarea' ? 'textarea' : fieldType === 'phone' ? 'tel' : fieldType,
|
|
63
63
|
label: label || humanLabel(key),
|
|
64
64
|
...(required ? { required: true } : {}),
|
|
65
65
|
});
|
|
@@ -119,12 +119,77 @@ function upsertSchemaList(sections, listPath, itemFields, items) {
|
|
|
119
119
|
maxItems: Math.max((items || []).length, 12),
|
|
120
120
|
fields: (itemFields || []).map((field) => ({
|
|
121
121
|
key: field.key,
|
|
122
|
-
type:
|
|
122
|
+
type:
|
|
123
|
+
field.type === 'url'
|
|
124
|
+
? 'url'
|
|
125
|
+
: field.type === 'image'
|
|
126
|
+
? 'image'
|
|
127
|
+
: field.type === 'textarea'
|
|
128
|
+
? 'textarea'
|
|
129
|
+
: field.type === 'tel' || field.type === 'phone'
|
|
130
|
+
? 'tel'
|
|
131
|
+
: field.type === 'email'
|
|
132
|
+
? 'email'
|
|
133
|
+
: field.type || 'text',
|
|
123
134
|
label: humanLabel(field.key),
|
|
124
135
|
})),
|
|
125
136
|
});
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
const LIST_ACTION_CTA_ITEM_FIELDS = [
|
|
140
|
+
{ key: 'buttonLabel', type: 'text' },
|
|
141
|
+
{ key: 'buttonUrl', type: 'url' },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
function isListActionCtaKey(key) {
|
|
145
|
+
return /Cta$/i.test(String(key || ''));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Walks merged site-data content and registers schemas ARC learns from merchant patterns:
|
|
150
|
+
* list CTAs (`*Cta` with buttonLabel/buttonUrl) and paired *Label/*Url siblings.
|
|
151
|
+
*/
|
|
152
|
+
function enrichSchemasFromContent(content, sections) {
|
|
153
|
+
if (!content || typeof content !== 'object') return;
|
|
154
|
+
|
|
155
|
+
function walk(node, prefix) {
|
|
156
|
+
if (!node || typeof node !== 'object') return;
|
|
157
|
+
if (Array.isArray(node)) {
|
|
158
|
+
const listKey = prefix.split('.').pop() || '';
|
|
159
|
+
if (isListActionCtaKey(listKey)) {
|
|
160
|
+
const sample = node[0];
|
|
161
|
+
if (sample && typeof sample === 'object' && ('buttonLabel' in sample || 'buttonUrl' in sample)) {
|
|
162
|
+
upsertSchemaList(sections, prefix, LIST_ACTION_CTA_ITEM_FIELDS, node);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
node.forEach((item, idx) => {
|
|
166
|
+
if (item && typeof item === 'object') walk(item, `${prefix}[${idx}]`);
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const [key, value] of Object.entries(node)) {
|
|
172
|
+
const nextPath = prefix ? `${prefix}.${key}` : key;
|
|
173
|
+
if (Array.isArray(value) && isListActionCtaKey(key)) {
|
|
174
|
+
const sample = value[0];
|
|
175
|
+
if (sample && typeof sample === 'object' && ('buttonLabel' in sample || 'buttonUrl' in sample)) {
|
|
176
|
+
upsertSchemaList(sections, nextPath, LIST_ACTION_CTA_ITEM_FIELDS, value);
|
|
177
|
+
}
|
|
178
|
+
} else if (typeof value === 'string' && /Url$/i.test(key)) {
|
|
179
|
+
upsertSchemaField(sections, nextPath, 'url', humanLabel(key));
|
|
180
|
+
const labelKey = key.replace(/Url$/i, 'Label');
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(node, labelKey)) {
|
|
182
|
+
upsertSchemaField(sections, `${prefix}.${labelKey}`, 'text', humanLabel(labelKey));
|
|
183
|
+
}
|
|
184
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
185
|
+
walk(value, nextPath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
walk(content, '');
|
|
191
|
+
}
|
|
192
|
+
|
|
128
193
|
function collectFieldsFromPlan(plan) {
|
|
129
194
|
const fields = [];
|
|
130
195
|
for (const file of plan.files || []) {
|
|
@@ -362,6 +427,7 @@ function buildSiteDataAndManifest({
|
|
|
362
427
|
siteData.template.structure.pages = routes.map((p) => p.id);
|
|
363
428
|
|
|
364
429
|
assignSectionPageKeys(editorSections, routes, markerRoutes);
|
|
430
|
+
enrichSchemasFromContent(content, editorSections);
|
|
365
431
|
|
|
366
432
|
const controlOnlyPaths = computeControlOnlyPaths(
|
|
367
433
|
content,
|
|
@@ -435,4 +501,6 @@ module.exports = {
|
|
|
435
501
|
computeControlOnlyPaths,
|
|
436
502
|
assignSectionPageKeys,
|
|
437
503
|
upsertSchemaList,
|
|
504
|
+
enrichSchemasFromContent,
|
|
505
|
+
isListActionCtaKey,
|
|
438
506
|
};
|
package/src/arc/transformer.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const recast = require('recast');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
5
6
|
const {
|
|
6
7
|
parseSource,
|
|
7
8
|
printSource,
|
|
@@ -12,6 +13,7 @@ const {
|
|
|
12
13
|
hasDirective,
|
|
13
14
|
ensureImport,
|
|
14
15
|
ensureDefaultImport,
|
|
16
|
+
sanitizeDuplicateBindings,
|
|
15
17
|
siteDataBinding,
|
|
16
18
|
siteDataListBinding,
|
|
17
19
|
jsxPreviewAttr,
|
|
@@ -124,13 +126,42 @@ function splitActionChildren(node, labelField, labelFallback) {
|
|
|
124
126
|
node.children = nextChildren;
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
function wrapHiddenUrlSibling(pathNode, urlField, fallback) {
|
|
130
|
+
const urlParts = urlField.split('.');
|
|
131
|
+
const hiddenUrl = b.jsxElement(
|
|
132
|
+
b.jsxOpeningElement(
|
|
133
|
+
b.jsxIdentifier('span'),
|
|
134
|
+
[
|
|
135
|
+
b.jsxAttribute(b.jsxIdentifier('hidden')),
|
|
136
|
+
b.jsxAttribute(b.jsxIdentifier('aria-hidden'), b.stringLiteral('true')),
|
|
137
|
+
jsxPreviewAttr(urlField),
|
|
138
|
+
],
|
|
139
|
+
false
|
|
140
|
+
),
|
|
141
|
+
b.jsxClosingElement(b.jsxIdentifier('span')),
|
|
142
|
+
[b.jsxExpressionContainer(siteDataBinding(urlParts, fallback, 'url'))],
|
|
143
|
+
false
|
|
144
|
+
);
|
|
145
|
+
pathNode.insertAfter(hiddenUrl);
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
function applyTransformToElement(pathNode, transform) {
|
|
128
149
|
const node = pathNode.node;
|
|
129
150
|
if (transform.operation === 'split-action-contract') {
|
|
130
151
|
const urlParts = transform.urlField.split('.');
|
|
131
152
|
replaceAttrValue(node, 'href', siteDataBinding(urlParts, transform.fallback, 'url'));
|
|
132
|
-
|
|
153
|
+
if (!hasJsxAttribute(node, 'data-preview-static')) {
|
|
154
|
+
node.openingElement.attributes.push(
|
|
155
|
+
b.jsxAttribute(b.jsxIdentifier('data-preview-static'), b.stringLiteral('action-link'))
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (hasJsxAttribute(node, 'data-preview-field-path')) {
|
|
159
|
+
node.openingElement.attributes = node.openingElement.attributes.filter(
|
|
160
|
+
(attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-field-path')
|
|
161
|
+
);
|
|
162
|
+
}
|
|
133
163
|
splitActionChildren(node, transform.labelField, transform.labelFallback || '');
|
|
164
|
+
wrapHiddenUrlSibling(pathNode, transform.urlField, transform.fallback);
|
|
134
165
|
return;
|
|
135
166
|
}
|
|
136
167
|
if (transform.operation === 'extract-url') {
|
|
@@ -370,6 +401,43 @@ function bindArrayDeclaration(ast, mapCallPath, listPath) {
|
|
|
370
401
|
return bound;
|
|
371
402
|
}
|
|
372
403
|
|
|
404
|
+
function fileAlreadyUsesSiteDataHook(ast) {
|
|
405
|
+
let found = false;
|
|
406
|
+
recast.types.visit(ast, {
|
|
407
|
+
visitCallExpression(pathNode) {
|
|
408
|
+
const callee = pathNode.node.callee;
|
|
409
|
+
if (callee && callee.type === 'Identifier' && callee.name === 'useSiteData') {
|
|
410
|
+
found = true;
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
this.traverse(pathNode);
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
return found;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resolveSiteDataRuntimeSpecifier(profile) {
|
|
420
|
+
const root = profile && profile.root;
|
|
421
|
+
if (!root) return '@deneb-ui/ui';
|
|
422
|
+
const candidates = [
|
|
423
|
+
['src/lib/siteDataContext.tsx', '@/lib/siteDataContext'],
|
|
424
|
+
['src/lib/siteDataContext.ts', '@/lib/siteDataContext'],
|
|
425
|
+
['lib/siteDataContext.tsx', '@/lib/siteDataContext'],
|
|
426
|
+
['lib/siteDataContext.ts', '@/lib/siteDataContext'],
|
|
427
|
+
];
|
|
428
|
+
const hasAt = Object.keys(profile.aliasMap || {}).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
429
|
+
for (const [relative, alias] of candidates) {
|
|
430
|
+
if (!fs.existsSync(path.join(root, relative))) continue;
|
|
431
|
+
if (hasAt) return alias;
|
|
432
|
+
const fromAbs = path.join(root, 'src/components/placeholder.tsx');
|
|
433
|
+
const toAbs = path.join(root, relative.replace(/\.tsx?$/, ''));
|
|
434
|
+
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
435
|
+
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
436
|
+
return relSpec;
|
|
437
|
+
}
|
|
438
|
+
return '@deneb-ui/ui';
|
|
439
|
+
}
|
|
440
|
+
|
|
373
441
|
function injectSiteDataHook(ast) {
|
|
374
442
|
const program = ast.program || ast;
|
|
375
443
|
let injected = false;
|
|
@@ -508,11 +576,15 @@ function applyFilePlan(filePlan, profile) {
|
|
|
508
576
|
|
|
509
577
|
const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
|
|
510
578
|
if (isClient) {
|
|
511
|
-
|
|
512
|
-
|
|
579
|
+
const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
|
|
580
|
+
if (!fileAlreadyUsesSiteDataHook(ast)) {
|
|
581
|
+
injectSiteDataHook(ast);
|
|
582
|
+
}
|
|
583
|
+
ensureImport(ast, runtimeSpecifier, ['useSiteData']);
|
|
513
584
|
} else {
|
|
514
585
|
ensureDefaultImport(ast, siteDataImport, 'siteData');
|
|
515
586
|
}
|
|
587
|
+
sanitizeDuplicateBindings(ast);
|
|
516
588
|
|
|
517
589
|
// Sanitize any conflicting data-preview-static on elements with editable markers
|
|
518
590
|
sanitizeContradictoryMarkers(ast);
|
|
@@ -610,57 +682,91 @@ function ensureHtmlBodyHydration(ast) {
|
|
|
610
682
|
});
|
|
611
683
|
}
|
|
612
684
|
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
685
|
+
function findSiteDataJsonLocalName(ast) {
|
|
686
|
+
const program = ast.program || ast;
|
|
687
|
+
for (const node of program.body || []) {
|
|
688
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
689
|
+
const src = node.source && node.source.value;
|
|
690
|
+
if (typeof src !== 'string' || !/site-data\.json/.test(src)) continue;
|
|
691
|
+
const spec = (node.specifiers || []).find((s) => s.type === 'ImportDefaultSpecifier');
|
|
692
|
+
if (spec && spec.local) return spec.local.name;
|
|
621
693
|
}
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
622
696
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
ensureImport(ast, '@deneb-ui/ui', ['SiteDataProvider']);
|
|
626
|
-
ensureDefaultImport(ast, siteDataImport, 'initialSiteData');
|
|
627
|
-
|
|
628
|
-
let wrapped = false;
|
|
697
|
+
function ensureProviderInitialData(ast, ident) {
|
|
698
|
+
let added = false;
|
|
629
699
|
recast.types.visit(ast, {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
if (
|
|
634
|
-
pathNode.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
),
|
|
646
|
-
b.jsxClosingElement(b.jsxIdentifier('SiteDataProvider')),
|
|
647
|
-
[pathNode.node],
|
|
648
|
-
false
|
|
649
|
-
)
|
|
650
|
-
);
|
|
651
|
-
wrapped = true;
|
|
652
|
-
return false;
|
|
700
|
+
visitJSXOpeningElement(pathNode) {
|
|
701
|
+
const name = pathNode.node.name;
|
|
702
|
+
const tag = name && name.type === 'JSXIdentifier' ? name.name : '';
|
|
703
|
+
if (tag === 'SiteDataProvider' || tag === 'DenebDataProvider' || tag === 'DenebSiteDataProvider') {
|
|
704
|
+
const has = hasJsxAttribute(pathNode.node, 'initialSiteData');
|
|
705
|
+
if (!has) {
|
|
706
|
+
pathNode.node.attributes = pathNode.node.attributes || [];
|
|
707
|
+
pathNode.node.attributes.push(
|
|
708
|
+
b.jsxAttribute(
|
|
709
|
+
b.jsxIdentifier('initialSiteData'),
|
|
710
|
+
b.jsxExpressionContainer(b.identifier(ident))
|
|
711
|
+
)
|
|
712
|
+
);
|
|
713
|
+
added = true;
|
|
714
|
+
}
|
|
653
715
|
}
|
|
654
716
|
this.traverse(pathNode);
|
|
655
717
|
},
|
|
656
718
|
});
|
|
719
|
+
return added;
|
|
720
|
+
}
|
|
657
721
|
|
|
658
|
-
|
|
722
|
+
const CANONICAL_SITE_DATA_CONTEXT = `'use client';
|
|
723
|
+
|
|
724
|
+
export {
|
|
725
|
+
SiteDataProvider,
|
|
726
|
+
useSiteData,
|
|
727
|
+
contentText,
|
|
728
|
+
contentObject,
|
|
729
|
+
contentList,
|
|
730
|
+
PREVIEW_DATA_MESSAGE,
|
|
731
|
+
LEGACY_PREVIEW_DATA_MESSAGE,
|
|
732
|
+
PREVIEW_READY_MESSAGE,
|
|
733
|
+
LEGACY_PREVIEW_READY_MESSAGE,
|
|
734
|
+
PREVIEW_FOCUS_MESSAGE,
|
|
735
|
+
LEGACY_PREVIEW_FOCUS_MESSAGE,
|
|
736
|
+
PREVIEW_FIELD_ATTRIBUTE,
|
|
737
|
+
} from '@deneb-ui/ui';
|
|
738
|
+
|
|
739
|
+
export type { SiteData, SiteDataProviderProps } from '@deneb-ui/ui';
|
|
740
|
+
`;
|
|
741
|
+
|
|
742
|
+
function rewriteRecursiveSiteDataContext(code) {
|
|
743
|
+
const recursive =
|
|
744
|
+
/export\s+function\s+SiteDataProvider\b/.test(code) &&
|
|
745
|
+
/<(?:Base)?SiteDataProvider\b/.test(code);
|
|
746
|
+
if (!recursive) return { code, updated: false };
|
|
747
|
+
return { code: CANONICAL_SITE_DATA_CONTEXT, updated: true };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-ui/ui') {
|
|
751
|
+
const ast = parseSource(code, 'layout.tsx');
|
|
752
|
+
ensureHtmlBodyHydration(ast);
|
|
753
|
+
|
|
754
|
+
let jsonIdent = findSiteDataJsonLocalName(ast);
|
|
755
|
+
if (!jsonIdent) {
|
|
756
|
+
jsonIdent = 'initialSiteData';
|
|
757
|
+
ensureDefaultImport(ast, siteDataImport, jsonIdent);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const hasProvider = /SiteDataProvider|DenebDataProvider/.test(code);
|
|
761
|
+
let wrapped = hasProvider;
|
|
762
|
+
|
|
763
|
+
if (!hasProvider) {
|
|
764
|
+
ensureImport(ast, providerImport, ['SiteDataProvider']);
|
|
659
765
|
recast.types.visit(ast, {
|
|
660
|
-
|
|
766
|
+
visitJSXExpressionContainer(pathNode) {
|
|
661
767
|
if (wrapped) return false;
|
|
662
|
-
const
|
|
663
|
-
if (name === '
|
|
768
|
+
const expr = pathNode.node.expression;
|
|
769
|
+
if (expr && expr.type === 'Identifier' && expr.name === 'children') {
|
|
664
770
|
pathNode.replace(
|
|
665
771
|
b.jsxElement(
|
|
666
772
|
b.jsxOpeningElement(
|
|
@@ -668,7 +774,7 @@ function instrumentLayoutSource(code, siteDataImport) {
|
|
|
668
774
|
[
|
|
669
775
|
b.jsxAttribute(
|
|
670
776
|
b.jsxIdentifier('initialSiteData'),
|
|
671
|
-
b.jsxExpressionContainer(b.identifier(
|
|
777
|
+
b.jsxExpressionContainer(b.identifier(jsonIdent))
|
|
672
778
|
),
|
|
673
779
|
],
|
|
674
780
|
false
|
|
@@ -684,9 +790,43 @@ function instrumentLayoutSource(code, siteDataImport) {
|
|
|
684
790
|
this.traverse(pathNode);
|
|
685
791
|
},
|
|
686
792
|
});
|
|
793
|
+
|
|
794
|
+
if (!wrapped) {
|
|
795
|
+
recast.types.visit(ast, {
|
|
796
|
+
visitJSXElement(pathNode) {
|
|
797
|
+
if (wrapped) return false;
|
|
798
|
+
const name = getJsxName(pathNode.node);
|
|
799
|
+
if (name === 'Component') {
|
|
800
|
+
pathNode.replace(
|
|
801
|
+
b.jsxElement(
|
|
802
|
+
b.jsxOpeningElement(
|
|
803
|
+
b.jsxIdentifier('SiteDataProvider'),
|
|
804
|
+
[
|
|
805
|
+
b.jsxAttribute(
|
|
806
|
+
b.jsxIdentifier('initialSiteData'),
|
|
807
|
+
b.jsxExpressionContainer(b.identifier(jsonIdent))
|
|
808
|
+
),
|
|
809
|
+
],
|
|
810
|
+
false
|
|
811
|
+
),
|
|
812
|
+
b.jsxClosingElement(b.jsxIdentifier('SiteDataProvider')),
|
|
813
|
+
[pathNode.node],
|
|
814
|
+
false
|
|
815
|
+
)
|
|
816
|
+
);
|
|
817
|
+
wrapped = true;
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
this.traverse(pathNode);
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
}
|
|
687
824
|
}
|
|
688
825
|
|
|
689
|
-
|
|
826
|
+
ensureProviderInitialData(ast, jsonIdent);
|
|
827
|
+
sanitizeDuplicateBindings(ast);
|
|
828
|
+
const next = printSource(ast, code);
|
|
829
|
+
return { code: next, updated: next !== code };
|
|
690
830
|
}
|
|
691
831
|
|
|
692
832
|
function ensureJsonModule(tsconfig) {
|
|
@@ -752,6 +892,8 @@ module.exports = {
|
|
|
752
892
|
instrumentLayoutSource,
|
|
753
893
|
instrumentPageKey,
|
|
754
894
|
resolveSiteDataSpecifier,
|
|
895
|
+
resolveSiteDataRuntimeSpecifier,
|
|
896
|
+
rewriteRecursiveSiteDataContext,
|
|
755
897
|
ensureJsonModule,
|
|
756
898
|
inferPageKey,
|
|
757
899
|
sanitizeContradictoryMarkers,
|
|
@@ -615,8 +615,8 @@ function transformFileContent(filePath, pageKey, extractedData, backupDir, proje
|
|
|
615
615
|
code = "'use client';\n\n" + code;
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
// Add useSiteData import if needed
|
|
619
|
-
if (
|
|
618
|
+
// Add useSiteData import if needed. Skip when any binding already exists.
|
|
619
|
+
if (!/\buseSiteData\b/.test(code)) {
|
|
620
620
|
code = code.replace(
|
|
621
621
|
/(import\s+[^;]+;\n)/,
|
|
622
622
|
`$1import { useSiteData } from '@deneb-ui/ui';\n`
|