@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.30",
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.30",
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 already = new Set(
273
- (existing.specifiers || [])
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 = names.map((name) => b.importSpecifier(b.identifier(name), b.identifier(name)));
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);
@@ -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
- ensureImport(ast, '@deneb-ui/ui', ['useSiteData']);
512
- injectSiteDataHook(ast);
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 instrumentLayoutSource(code, siteDataImport) {
614
- if (/SiteDataProvider|DenebDataProvider/.test(code)) {
615
- if (/suppressHydrationWarning/.test(code)) {
616
- return { code, updated: false };
617
- }
618
- const ast = parseSource(code, 'layout.tsx');
619
- ensureHtmlBodyHydration(ast);
620
- return { code: printSource(ast, code), updated: true };
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
- const ast = parseSource(code, 'layout.tsx');
624
- ensureHtmlBodyHydration(ast);
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
- visitJSXExpressionContainer(pathNode) {
631
- if (wrapped) return false;
632
- const expr = pathNode.node.expression;
633
- if (expr && expr.type === 'Identifier' && expr.name === 'children') {
634
- pathNode.replace(
635
- b.jsxElement(
636
- b.jsxOpeningElement(
637
- b.jsxIdentifier('SiteDataProvider'),
638
- [
639
- b.jsxAttribute(
640
- b.jsxIdentifier('initialSiteData'),
641
- b.jsxExpressionContainer(b.identifier('initialSiteData'))
642
- ),
643
- ],
644
- false
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
- if (!wrapped) {
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
- visitJSXElement(pathNode) {
737
+ visitJSXExpressionContainer(pathNode) {
661
738
  if (wrapped) return false;
662
- const name = getJsxName(pathNode.node);
663
- if (name === 'Component') {
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('initialSiteData'))
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
- return { code: printSource(ast, code), updated: wrapped };
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 (!code.includes('useSiteData')) {
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`