@deneb-ui/cli 2.0.29 → 2.0.31

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 CHANGED
@@ -732,12 +732,12 @@ function initProject(targetInput, options = {}) {
732
732
  // 6. Install DENEB packages if missing
733
733
  const skipInstall = process.argv.includes('--skip-install');
734
734
  const hasUi = Boolean(
735
- (pkg.dependencies && (pkg.dependencies['@deneb-ui/ui'] || pkg.dependencies['@deneb/ui'] || pkg.dependencies['@fivora/editable-components'])) ||
736
- (pkg.devDependencies && (pkg.devDependencies['@deneb-ui/ui'] || pkg.devDependencies['@deneb/ui'] || pkg.devDependencies['@fivora/editable-components']))
735
+ (pkg.dependencies && (pkg.dependencies['@deneb-ui/ui'] || pkg.dependencies['@deneb/ui'])) ||
736
+ (pkg.devDependencies && (pkg.devDependencies['@deneb-ui/ui'] || pkg.devDependencies['@deneb/ui']))
737
737
  );
738
738
  const hasCli = Boolean(
739
- (pkg.devDependencies && (pkg.devDependencies['@deneb-ui/cli'] || pkg.devDependencies['@fivora/cli'])) ||
740
- (pkg.dependencies && (pkg.dependencies['@deneb-ui/cli'] || pkg.dependencies['@fivora/cli']))
739
+ (pkg.devDependencies && (pkg.devDependencies['@deneb-ui/cli'] || pkg.devDependencies['@deneb/cli'])) ||
740
+ (pkg.dependencies && (pkg.dependencies['@deneb-ui/cli'] || pkg.dependencies['@deneb/cli']))
741
741
  );
742
742
 
743
743
  if (!skipInstall && (!hasUi || !hasCli)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deneb-ui/cli",
3
- "version": "2.0.29",
3
+ "version": "2.0.31",
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.29",
52
+ "@deneb-ui/core": "^2.0.31",
53
53
  "adm-zip": "^0.6.0",
54
54
  "recast": "^0.23.11"
55
55
  },
@@ -496,3 +496,26 @@ test('schema sections only claim a pageKey when reachability proves it', () => {
496
496
  assert.equal(sections.find((s) => s.id === 'home').pageKey, 'home');
497
497
  assert.equal(sections.find((s) => s.id === 'about').pageKey, 'about');
498
498
  });
499
+
500
+ test('conflicting data-preview-static is stripped when element has editable markers', () => {
501
+ const dir = copyOf(STOREFRONT_FIXTURE);
502
+ // Introduce a conflicting static marker on an element with an editable marker
503
+ const heroPath = path.join(dir, 'src', 'components', 'Hero.tsx');
504
+ let heroCode = fs.readFileSync(heroPath, 'utf8');
505
+ heroCode = heroCode.replace(
506
+ '<p className="mt-4',
507
+ '<p data-preview-static="legacy static description" className="mt-4'
508
+ );
509
+ fs.writeFileSync(heroPath, heroCode, 'utf8');
510
+
511
+ silence(() => runDenebArc(dir, 'acme-store', { telemetry: 'off' }));
512
+ const updatedHero = fs.readFileSync(heroPath, 'utf8');
513
+
514
+ // data-preview-static must be stripped since the paragraph has data-preview-field-path
515
+ assert.ok(!updatedHero.includes('data-preview-static="legacy static description"'));
516
+ assert.ok(updatedHero.includes('data-preview-field-path'));
517
+
518
+ const { errors } = auditFivora(dir);
519
+ const placementErrors = errors.filter((e) => e.includes('cannot share an element with data-preview-static'));
520
+ assert.equal(placementErrors.length, 0);
521
+ });
package/src/arc/ast.cjs CHANGED
@@ -267,22 +267,30 @@ function hasDirective(ast, value) {
267
267
  function ensureImport(ast, source, names) {
268
268
  const program = ast.program || ast;
269
269
  const body = program.body || [];
270
+
271
+ // Deduplicate against any import in the entire module so we never import the same identifier twice
272
+ const alreadyImported = new Set();
273
+ for (const node of body) {
274
+ if (node.type === 'ImportDeclaration' && Array.isArray(node.specifiers)) {
275
+ for (const spec of node.specifiers) {
276
+ const local = spec.local?.name || spec.imported?.name;
277
+ if (local) alreadyImported.add(local);
278
+ }
279
+ }
280
+ }
281
+
282
+ const namesToImport = names.filter((name) => !alreadyImported.has(name));
283
+ if (namesToImport.length === 0) return;
284
+
270
285
  const existing = body.find((node) => node.type === 'ImportDeclaration' && node.source && node.source.value === source);
271
286
  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
- }
287
+ for (const name of namesToImport) {
288
+ existing.specifiers.push(b.importSpecifier(b.identifier(name), b.identifier(name)));
281
289
  }
282
290
  return;
283
291
  }
284
292
 
285
- const specifiers = names.map((name) => b.importSpecifier(b.identifier(name), b.identifier(name)));
293
+ const specifiers = namesToImport.map((name) => b.importSpecifier(b.identifier(name), b.identifier(name)));
286
294
  const decl = b.importDeclaration(specifiers, b.stringLiteral(source));
287
295
  let insertAt = 0;
288
296
  if (body[0] && body[0].type === 'ExpressionStatement') insertAt = 1;
@@ -490,6 +490,7 @@ function findUncoveredVisibleText(code, filePath) {
490
490
  // A `<` preceded by an identifier character is a TypeScript generic
491
491
  // (forwardRef<HTMLButtonElement, Props>), never a JSX element.
492
492
  if (/[\w$)\]]/.test(source[offset - 1] || '')) continue;
493
+ if (tag.toLowerCase() === 'option') continue;
493
494
  if (!text || text.length <= 2 || !/\p{L}/u.test(text)) continue;
494
495
  if (!/^[\p{L}\p{N}"'(¡¿#$€£]/u.test(text)) continue;
495
496
  if (/^(?:true|false|null|undefined)$/i.test(text)) continue;
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 } = require('./transformer.cjs');
20
+ const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, 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');
@@ -355,6 +355,19 @@ function runDenebArc(projectDir, projectName, options = {}) {
355
355
  }
356
356
  }
357
357
 
358
+ // Remove any conflicting data-preview-static from elements carrying editable markers
359
+ for (const relativeFile of profile.jsxFiles || []) {
360
+ const abs = path.join(projectDir, relativeFile);
361
+ if (!fs.existsSync(abs)) continue;
362
+ const original = fs.readFileSync(abs, 'utf8');
363
+ const sanitized = sanitizeContradictoryMarkersInSource(original, relativeFile);
364
+ if (sanitized.updated && sanitized.code !== original) {
365
+ backupFile(projectDir, backupDir, abs);
366
+ fs.writeFileSync(abs, sanitized.code, 'utf8');
367
+ if (!changedFiles.includes(relativeFile)) changedFiles.push(relativeFile);
368
+ }
369
+ }
370
+
358
371
  if (profile.framework === 'nextjs') {
359
372
  const before = findNextConfig(projectDir);
360
373
  if (before) backupFile(projectDir, backupDir, before.abs);
@@ -48,7 +48,16 @@ function replaceAttrValue(node, attrName, expression) {
48
48
  attr.value = b.jsxExpressionContainer(expression);
49
49
  }
50
50
 
51
+ function stripStaticAttribute(node) {
52
+ if (node && node.openingElement && Array.isArray(node.openingElement.attributes)) {
53
+ node.openingElement.attributes = node.openingElement.attributes.filter(
54
+ (attr) => !(attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'data-preview-static')
55
+ );
56
+ }
57
+ }
58
+
51
59
  function ensurePreviewPath(node, fieldPath) {
60
+ stripStaticAttribute(node);
52
61
  if (hasJsxAttribute(node, 'data-preview-field-path')) return;
53
62
  node.openingElement.attributes.push(jsxPreviewAttr(fieldPath));
54
63
  }
@@ -505,6 +514,9 @@ function applyFilePlan(filePlan, profile) {
505
514
  ensureDefaultImport(ast, siteDataImport, 'siteData');
506
515
  }
507
516
 
517
+ // Sanitize any conflicting data-preview-static on elements with editable markers
518
+ sanitizeContradictoryMarkers(ast);
519
+
508
520
  // Page keys are stamped in a separate route-driven pass so App Router and
509
521
  // Pages Router projects are handled by the same logic.
510
522
  const code = printSource(ast, filePlan.originalCode);
@@ -692,6 +704,49 @@ function ensureJsonModule(tsconfig) {
692
704
  };
693
705
  }
694
706
 
707
+ function sanitizeContradictoryMarkers(ast) {
708
+ let cleaned = 0;
709
+ recast.types.visit(ast, {
710
+ visitJSXOpeningElement(pathNode) {
711
+ const attrs = pathNode.node.attributes || [];
712
+ const hasEditable = attrs.some(
713
+ (a) => a.type === 'JSXAttribute' && a.name && (
714
+ a.name.name === 'data-preview-field-path' ||
715
+ a.name.name === 'data-preview-list-path' ||
716
+ a.name.name === 'data-preview-item-path'
717
+ )
718
+ );
719
+ const hasStatic = attrs.some(
720
+ (a) => a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static'
721
+ );
722
+ if (hasEditable && hasStatic) {
723
+ pathNode.node.attributes = attrs.filter(
724
+ (a) => !(a.type === 'JSXAttribute' && a.name && a.name.name === 'data-preview-static')
725
+ );
726
+ cleaned++;
727
+ }
728
+ this.traverse(pathNode);
729
+ },
730
+ });
731
+ return cleaned;
732
+ }
733
+
734
+ function sanitizeContradictoryMarkersInSource(code, relativeFile) {
735
+ if (!code.includes('data-preview-static')) return { code, updated: false };
736
+ if (!code.includes('data-preview-field-path') && !code.includes('data-preview-list-path') && !code.includes('data-preview-item-path')) {
737
+ return { code, updated: false };
738
+ }
739
+ let ast;
740
+ try {
741
+ ast = parseSource(code, relativeFile);
742
+ } catch {
743
+ return { code, updated: false };
744
+ }
745
+ const count = sanitizeContradictoryMarkers(ast);
746
+ if (count === 0) return { code, updated: false };
747
+ return { code: printSource(ast, code), updated: true, count };
748
+ }
749
+
695
750
  module.exports = {
696
751
  applyFilePlan,
697
752
  instrumentLayoutSource,
@@ -699,4 +754,6 @@ module.exports = {
699
754
  resolveSiteDataSpecifier,
700
755
  ensureJsonModule,
701
756
  inferPageKey,
757
+ sanitizeContradictoryMarkers,
758
+ sanitizeContradictoryMarkersInSource,
702
759
  };