@deneb-ui/cli 2.0.30 → 2.0.32
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 +59 -0
- package/src/arc/ast.cjs +156 -10
- package/src/arc/index.cjs +20 -2
- package/src/arc/transformer.cjs +158 -45
- 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.32",
|
|
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.32",
|
|
53
53
|
"adm-zip": "^0.6.0",
|
|
54
54
|
"recast": "^0.23.11"
|
|
55
55
|
},
|
|
@@ -519,3 +519,62 @@ test('conflicting data-preview-static is stripped when element has editable mark
|
|
|
519
519
|
const placementErrors = errors.filter((e) => e.includes('cannot share an element with data-preview-static'));
|
|
520
520
|
assert.equal(placementErrors.length, 0);
|
|
521
521
|
});
|
|
522
|
+
|
|
523
|
+
test('sanitizeDuplicateBindings keeps a single useSiteData import', () => {
|
|
524
|
+
const { parseSource, printSource, sanitizeDuplicateBindings } = require('../ast.cjs');
|
|
525
|
+
const code = `'use client';
|
|
526
|
+
import { useSiteData, contentText } from '@/lib/siteDataContext';
|
|
527
|
+
import { useSiteData, contentObject } from '@deneb-ui/ui';
|
|
528
|
+
export function Shop() {
|
|
529
|
+
const siteData = useSiteData();
|
|
530
|
+
return <div>{contentText(contentObject(siteData).title)}</div>;
|
|
531
|
+
}
|
|
532
|
+
`;
|
|
533
|
+
const ast = parseSource(code, 'shop.tsx');
|
|
534
|
+
sanitizeDuplicateBindings(ast);
|
|
535
|
+
const out = printSource(ast, code);
|
|
536
|
+
const importUses = [...out.matchAll(/import\s*\{([^}]+)\}\s*from/g)].flatMap((m) =>
|
|
537
|
+
m[1].split(',').map((s) => s.trim()).filter((s) => s === 'useSiteData')
|
|
538
|
+
);
|
|
539
|
+
assert.equal(importUses.length, 1);
|
|
540
|
+
assert.match(out, /siteDataContext/);
|
|
541
|
+
assert.doesNotMatch(out, /import\s*\{[^}]*useSiteData[^}]*\}\s*from\s*['"]@deneb-ui\/ui['"]/);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('recursive SiteDataProvider wrappers are flattened to a package re-export', () => {
|
|
545
|
+
const { rewriteRecursiveSiteDataContext } = require('../transformer.cjs');
|
|
546
|
+
const code = `'use client';
|
|
547
|
+
import { SiteDataProvider as BaseSiteDataProvider } from '@deneb-ui/ui';
|
|
548
|
+
import initialSiteData from '@/data/site-data.json';
|
|
549
|
+
export function SiteDataProvider({ children, ...props }) {
|
|
550
|
+
return (
|
|
551
|
+
<BaseSiteDataProvider initialSiteData={initialSiteData} {...props}>
|
|
552
|
+
{children}
|
|
553
|
+
</BaseSiteDataProvider>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
`;
|
|
557
|
+
const out = rewriteRecursiveSiteDataContext(code);
|
|
558
|
+
assert.equal(out.updated, true);
|
|
559
|
+
assert.match(out.code, /export \{/);
|
|
560
|
+
assert.match(out.code, /SiteDataProvider/);
|
|
561
|
+
assert.match(out.code, /from '@deneb-ui\/ui'/);
|
|
562
|
+
assert.doesNotMatch(out.code, /export function SiteDataProvider/);
|
|
563
|
+
assert.doesNotMatch(out.code, /BaseSiteDataProvider/);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('import plus export of useSiteData is rewritten to export-from', () => {
|
|
567
|
+
const { parseSource, printSource, sanitizeDuplicateBindings } = require('../ast.cjs');
|
|
568
|
+
const code = `'use client';
|
|
569
|
+
import { SiteDataProvider as BaseSiteDataProvider, useSiteData, contentText } from '@deneb-ui/ui';
|
|
570
|
+
export function SiteDataProvider({ children }) {
|
|
571
|
+
return <BaseSiteDataProvider>{children}</BaseSiteDataProvider>;
|
|
572
|
+
}
|
|
573
|
+
export { useSiteData, contentText };
|
|
574
|
+
`;
|
|
575
|
+
const ast = parseSource(code, 'siteDataContext.tsx');
|
|
576
|
+
sanitizeDuplicateBindings(ast);
|
|
577
|
+
const out = printSource(ast, code);
|
|
578
|
+
assert.match(out, /export\s*\{[^}]*useSiteData[^}]*\}\s*from\s*['"]@deneb-ui\/ui['"]/);
|
|
579
|
+
assert.doesNotMatch(out, /import\s*\{[^}]*useSiteData/);
|
|
580
|
+
});
|
package/src/arc/ast.cjs
CHANGED
|
@@ -264,25 +264,170 @@ 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 || [];
|
|
404
|
+
|
|
405
|
+
const alreadyImported = new Set();
|
|
406
|
+
for (const node of body) {
|
|
407
|
+
if (node.type === 'ImportDeclaration' && Array.isArray(node.specifiers)) {
|
|
408
|
+
for (const spec of node.specifiers) {
|
|
409
|
+
const local = spec.local?.name || spec.imported?.name;
|
|
410
|
+
if (local) alreadyImported.add(local);
|
|
411
|
+
}
|
|
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
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const namesToImport = names.filter((name) => !alreadyImported.has(name));
|
|
420
|
+
if (namesToImport.length === 0) return;
|
|
421
|
+
|
|
270
422
|
const existing = body.find((node) => node.type === 'ImportDeclaration' && node.source && node.source.value === source);
|
|
271
423
|
if (existing) {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
.filter((s) => s.type === 'ImportSpecifier')
|
|
275
|
-
.map((s) => s.imported?.name || s.local?.name)
|
|
276
|
-
);
|
|
277
|
-
for (const name of names) {
|
|
278
|
-
if (!already.has(name)) {
|
|
279
|
-
existing.specifiers.push(b.importSpecifier(b.identifier(name), b.identifier(name)));
|
|
280
|
-
}
|
|
424
|
+
for (const name of namesToImport) {
|
|
425
|
+
existing.specifiers.push(b.importSpecifier(b.identifier(name), b.identifier(name)));
|
|
281
426
|
}
|
|
282
427
|
return;
|
|
283
428
|
}
|
|
284
429
|
|
|
285
|
-
const specifiers =
|
|
430
|
+
const specifiers = namesToImport.map((name) => b.importSpecifier(b.identifier(name), b.identifier(name)));
|
|
286
431
|
const decl = b.importDeclaration(specifiers, b.stringLiteral(source));
|
|
287
432
|
let insertAt = 0;
|
|
288
433
|
if (body[0] && body[0].type === 'ExpressionStatement') insertAt = 1;
|
|
@@ -332,6 +477,7 @@ module.exports = {
|
|
|
332
477
|
hasDirective,
|
|
333
478
|
ensureImport,
|
|
334
479
|
ensureDefaultImport,
|
|
480
|
+
sanitizeDuplicateBindings,
|
|
335
481
|
codeHasIdentifier,
|
|
336
482
|
t,
|
|
337
483
|
b,
|
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/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,
|
|
@@ -370,6 +372,43 @@ function bindArrayDeclaration(ast, mapCallPath, listPath) {
|
|
|
370
372
|
return bound;
|
|
371
373
|
}
|
|
372
374
|
|
|
375
|
+
function fileAlreadyUsesSiteDataHook(ast) {
|
|
376
|
+
let found = false;
|
|
377
|
+
recast.types.visit(ast, {
|
|
378
|
+
visitCallExpression(pathNode) {
|
|
379
|
+
const callee = pathNode.node.callee;
|
|
380
|
+
if (callee && callee.type === 'Identifier' && callee.name === 'useSiteData') {
|
|
381
|
+
found = true;
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
this.traverse(pathNode);
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
return found;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function resolveSiteDataRuntimeSpecifier(profile) {
|
|
391
|
+
const root = profile && profile.root;
|
|
392
|
+
if (!root) return '@deneb-ui/ui';
|
|
393
|
+
const candidates = [
|
|
394
|
+
['src/lib/siteDataContext.tsx', '@/lib/siteDataContext'],
|
|
395
|
+
['src/lib/siteDataContext.ts', '@/lib/siteDataContext'],
|
|
396
|
+
['lib/siteDataContext.tsx', '@/lib/siteDataContext'],
|
|
397
|
+
['lib/siteDataContext.ts', '@/lib/siteDataContext'],
|
|
398
|
+
];
|
|
399
|
+
const hasAt = Object.keys(profile.aliasMap || {}).some((k) => k === '@/*' || k.startsWith('@/'));
|
|
400
|
+
for (const [relative, alias] of candidates) {
|
|
401
|
+
if (!fs.existsSync(path.join(root, relative))) continue;
|
|
402
|
+
if (hasAt) return alias;
|
|
403
|
+
const fromAbs = path.join(root, 'src/components/placeholder.tsx');
|
|
404
|
+
const toAbs = path.join(root, relative.replace(/\.tsx?$/, ''));
|
|
405
|
+
let relSpec = path.relative(path.dirname(fromAbs), toAbs).replace(/\\/g, '/');
|
|
406
|
+
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
407
|
+
return relSpec;
|
|
408
|
+
}
|
|
409
|
+
return '@deneb-ui/ui';
|
|
410
|
+
}
|
|
411
|
+
|
|
373
412
|
function injectSiteDataHook(ast) {
|
|
374
413
|
const program = ast.program || ast;
|
|
375
414
|
let injected = false;
|
|
@@ -508,11 +547,15 @@ function applyFilePlan(filePlan, profile) {
|
|
|
508
547
|
|
|
509
548
|
const siteDataImport = resolveSiteDataSpecifier(profile, filePlan.file);
|
|
510
549
|
if (isClient) {
|
|
511
|
-
|
|
512
|
-
|
|
550
|
+
const runtimeSpecifier = resolveSiteDataRuntimeSpecifier(profile);
|
|
551
|
+
if (!fileAlreadyUsesSiteDataHook(ast)) {
|
|
552
|
+
injectSiteDataHook(ast);
|
|
553
|
+
}
|
|
554
|
+
ensureImport(ast, runtimeSpecifier, ['useSiteData']);
|
|
513
555
|
} else {
|
|
514
556
|
ensureDefaultImport(ast, siteDataImport, 'siteData');
|
|
515
557
|
}
|
|
558
|
+
sanitizeDuplicateBindings(ast);
|
|
516
559
|
|
|
517
560
|
// Sanitize any conflicting data-preview-static on elements with editable markers
|
|
518
561
|
sanitizeContradictoryMarkers(ast);
|
|
@@ -610,57 +653,91 @@ function ensureHtmlBodyHydration(ast) {
|
|
|
610
653
|
});
|
|
611
654
|
}
|
|
612
655
|
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
656
|
+
function findSiteDataJsonLocalName(ast) {
|
|
657
|
+
const program = ast.program || ast;
|
|
658
|
+
for (const node of program.body || []) {
|
|
659
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
660
|
+
const src = node.source && node.source.value;
|
|
661
|
+
if (typeof src !== 'string' || !/site-data\.json/.test(src)) continue;
|
|
662
|
+
const spec = (node.specifiers || []).find((s) => s.type === 'ImportDefaultSpecifier');
|
|
663
|
+
if (spec && spec.local) return spec.local.name;
|
|
621
664
|
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
622
667
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
ensureImport(ast, '@deneb-ui/ui', ['SiteDataProvider']);
|
|
626
|
-
ensureDefaultImport(ast, siteDataImport, 'initialSiteData');
|
|
627
|
-
|
|
628
|
-
let wrapped = false;
|
|
668
|
+
function ensureProviderInitialData(ast, ident) {
|
|
669
|
+
let added = false;
|
|
629
670
|
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;
|
|
671
|
+
visitJSXOpeningElement(pathNode) {
|
|
672
|
+
const name = pathNode.node.name;
|
|
673
|
+
const tag = name && name.type === 'JSXIdentifier' ? name.name : '';
|
|
674
|
+
if (tag === 'SiteDataProvider' || tag === 'DenebDataProvider' || tag === 'DenebSiteDataProvider') {
|
|
675
|
+
const has = hasJsxAttribute(pathNode.node, 'initialSiteData');
|
|
676
|
+
if (!has) {
|
|
677
|
+
pathNode.node.attributes = pathNode.node.attributes || [];
|
|
678
|
+
pathNode.node.attributes.push(
|
|
679
|
+
b.jsxAttribute(
|
|
680
|
+
b.jsxIdentifier('initialSiteData'),
|
|
681
|
+
b.jsxExpressionContainer(b.identifier(ident))
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
added = true;
|
|
685
|
+
}
|
|
653
686
|
}
|
|
654
687
|
this.traverse(pathNode);
|
|
655
688
|
},
|
|
656
689
|
});
|
|
690
|
+
return added;
|
|
691
|
+
}
|
|
657
692
|
|
|
658
|
-
|
|
693
|
+
const CANONICAL_SITE_DATA_CONTEXT = `'use client';
|
|
694
|
+
|
|
695
|
+
export {
|
|
696
|
+
SiteDataProvider,
|
|
697
|
+
useSiteData,
|
|
698
|
+
contentText,
|
|
699
|
+
contentObject,
|
|
700
|
+
contentList,
|
|
701
|
+
PREVIEW_DATA_MESSAGE,
|
|
702
|
+
LEGACY_PREVIEW_DATA_MESSAGE,
|
|
703
|
+
PREVIEW_READY_MESSAGE,
|
|
704
|
+
LEGACY_PREVIEW_READY_MESSAGE,
|
|
705
|
+
PREVIEW_FOCUS_MESSAGE,
|
|
706
|
+
LEGACY_PREVIEW_FOCUS_MESSAGE,
|
|
707
|
+
PREVIEW_FIELD_ATTRIBUTE,
|
|
708
|
+
} from '@deneb-ui/ui';
|
|
709
|
+
|
|
710
|
+
export type { SiteData, SiteDataProviderProps } from '@deneb-ui/ui';
|
|
711
|
+
`;
|
|
712
|
+
|
|
713
|
+
function rewriteRecursiveSiteDataContext(code) {
|
|
714
|
+
const recursive =
|
|
715
|
+
/export\s+function\s+SiteDataProvider\b/.test(code) &&
|
|
716
|
+
/<(?:Base)?SiteDataProvider\b/.test(code);
|
|
717
|
+
if (!recursive) return { code, updated: false };
|
|
718
|
+
return { code: CANONICAL_SITE_DATA_CONTEXT, updated: true };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function instrumentLayoutSource(code, siteDataImport, providerImport = '@deneb-ui/ui') {
|
|
722
|
+
const ast = parseSource(code, 'layout.tsx');
|
|
723
|
+
ensureHtmlBodyHydration(ast);
|
|
724
|
+
|
|
725
|
+
let jsonIdent = findSiteDataJsonLocalName(ast);
|
|
726
|
+
if (!jsonIdent) {
|
|
727
|
+
jsonIdent = 'initialSiteData';
|
|
728
|
+
ensureDefaultImport(ast, siteDataImport, jsonIdent);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const hasProvider = /SiteDataProvider|DenebDataProvider/.test(code);
|
|
732
|
+
let wrapped = hasProvider;
|
|
733
|
+
|
|
734
|
+
if (!hasProvider) {
|
|
735
|
+
ensureImport(ast, providerImport, ['SiteDataProvider']);
|
|
659
736
|
recast.types.visit(ast, {
|
|
660
|
-
|
|
737
|
+
visitJSXExpressionContainer(pathNode) {
|
|
661
738
|
if (wrapped) return false;
|
|
662
|
-
const
|
|
663
|
-
if (name === '
|
|
739
|
+
const expr = pathNode.node.expression;
|
|
740
|
+
if (expr && expr.type === 'Identifier' && expr.name === 'children') {
|
|
664
741
|
pathNode.replace(
|
|
665
742
|
b.jsxElement(
|
|
666
743
|
b.jsxOpeningElement(
|
|
@@ -668,7 +745,7 @@ function instrumentLayoutSource(code, siteDataImport) {
|
|
|
668
745
|
[
|
|
669
746
|
b.jsxAttribute(
|
|
670
747
|
b.jsxIdentifier('initialSiteData'),
|
|
671
|
-
b.jsxExpressionContainer(b.identifier(
|
|
748
|
+
b.jsxExpressionContainer(b.identifier(jsonIdent))
|
|
672
749
|
),
|
|
673
750
|
],
|
|
674
751
|
false
|
|
@@ -684,9 +761,43 @@ function instrumentLayoutSource(code, siteDataImport) {
|
|
|
684
761
|
this.traverse(pathNode);
|
|
685
762
|
},
|
|
686
763
|
});
|
|
764
|
+
|
|
765
|
+
if (!wrapped) {
|
|
766
|
+
recast.types.visit(ast, {
|
|
767
|
+
visitJSXElement(pathNode) {
|
|
768
|
+
if (wrapped) return false;
|
|
769
|
+
const name = getJsxName(pathNode.node);
|
|
770
|
+
if (name === 'Component') {
|
|
771
|
+
pathNode.replace(
|
|
772
|
+
b.jsxElement(
|
|
773
|
+
b.jsxOpeningElement(
|
|
774
|
+
b.jsxIdentifier('SiteDataProvider'),
|
|
775
|
+
[
|
|
776
|
+
b.jsxAttribute(
|
|
777
|
+
b.jsxIdentifier('initialSiteData'),
|
|
778
|
+
b.jsxExpressionContainer(b.identifier(jsonIdent))
|
|
779
|
+
),
|
|
780
|
+
],
|
|
781
|
+
false
|
|
782
|
+
),
|
|
783
|
+
b.jsxClosingElement(b.jsxIdentifier('SiteDataProvider')),
|
|
784
|
+
[pathNode.node],
|
|
785
|
+
false
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
wrapped = true;
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
this.traverse(pathNode);
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
}
|
|
687
795
|
}
|
|
688
796
|
|
|
689
|
-
|
|
797
|
+
ensureProviderInitialData(ast, jsonIdent);
|
|
798
|
+
sanitizeDuplicateBindings(ast);
|
|
799
|
+
const next = printSource(ast, code);
|
|
800
|
+
return { code: next, updated: next !== code };
|
|
690
801
|
}
|
|
691
802
|
|
|
692
803
|
function ensureJsonModule(tsconfig) {
|
|
@@ -752,6 +863,8 @@ module.exports = {
|
|
|
752
863
|
instrumentLayoutSource,
|
|
753
864
|
instrumentPageKey,
|
|
754
865
|
resolveSiteDataSpecifier,
|
|
866
|
+
resolveSiteDataRuntimeSpecifier,
|
|
867
|
+
rewriteRecursiveSiteDataContext,
|
|
755
868
|
ensureJsonModule,
|
|
756
869
|
inferPageKey,
|
|
757
870
|
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`
|