@deneb-ui/cli 2.0.93 → 2.0.95

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.93",
3
+ "version": "2.0.95",
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.93",
52
+ "@deneb-ui/core": "^2.0.95",
53
53
  "@octokit/rest": "^22.0.1",
54
54
  "adm-zip": "^0.6.0",
55
55
  "dotenv": "^17.4.2",
@@ -32,7 +32,9 @@ test('DENEB preflight rejects a Next router destination with a duplicated base p
32
32
  fs.mkdirSync(componentDir, { recursive: true });
33
33
  fs.writeFileSync(
34
34
  path.join(componentDir, 'BrokenNavigation.tsx'),
35
- "router.push(withBasePath(pageRoute('about')));\n"
35
+ "import { useRouter as useStorefrontRouter } from 'next/navigation';\n" +
36
+ 'const storefrontNav = useStorefrontRouter();\n' +
37
+ "storefrontNav.replace(prefixBasePath(pageRoute('about')));\n"
36
38
  );
37
39
 
38
40
  try {
@@ -52,7 +54,7 @@ test('DENEB preflight rejects a Next router destination with a duplicated base p
52
54
  output,
53
55
  /Template navigation validation failed before dependency installation\./
54
56
  );
55
- assert.match(output, /src\/components\/BrokenNavigation\.tsx:1/);
57
+ assert.match(output, /src\/components\/BrokenNavigation\.tsx:3/);
56
58
  assert.doesNotMatch(output, /Install dependencies/);
57
59
  } finally {
58
60
  fs.rmSync(tempDir, { recursive: true, force: true });
@@ -3922,4 +3924,3 @@ test('ARC v3 Phase 18: formatBlockedExplanationTerminal renders actionable remed
3922
3924
 
3923
3925
 
3924
3926
 
3925
-
@@ -83,7 +83,7 @@ function evaluateAcceptanceGates(metrics = {}) {
83
83
  },
84
84
  designPreservation: {
85
85
  name: ACCEPTANCE_GATES.DESIGN_PRESERVATION,
86
- passed: Boolean((metrics.designPreservationScore ?? 100) >= 98),
86
+ passed: Boolean((metrics.designPreservationScore ?? 100) >= 95),
87
87
  critical: false,
88
88
  },
89
89
  controlOnlyProtection: {
@@ -58,9 +58,10 @@ function auditProjectAssets(projectDir, siteData = {}) {
58
58
  return;
59
59
  }
60
60
 
61
- // Must look like a path or filename (starts with / or has a file extension)
62
- if (!trimmed.startsWith('/') && !/\.[a-zA-Z0-9]+$/.test(trimmed)) {
63
- return;
61
+ // Must look like an asset file with a recognized media/font/file extension
62
+ const cleanPath = trimmed.split('?')[0].split('#')[0];
63
+ if (!/\.(png|jpe?g|svg|webp|gif|avif|ico|mp4|webm|pdf|woff2?|otf|ttf)$/i.test(cleanPath)) {
64
+ return; // Route path / navigation link, not a static public asset
64
65
  }
65
66
 
66
67
  // Normalize leading slash
@@ -88,8 +89,8 @@ function auditProjectAssets(projectDir, siteData = {}) {
88
89
  const fieldPath = currentPath ? `${currentPath}.${key}` : key;
89
90
  const lowerKey = key.toLowerCase();
90
91
  const isTextKey = lowerKey.includes('text') || lowerKey.includes('title') || lowerKey.includes('label') || lowerKey.includes('desc') || lowerKey.includes('alt') || lowerKey.includes('phone') || lowerKey.includes('whatsapp') || lowerKey.includes('email');
91
- const isAssetKey = !isTextKey && (lowerKey.includes('image') || lowerKey.includes('logo') || lowerKey.includes('avatar') || lowerKey.includes('icon') || lowerKey.includes('src') || lowerKey.endsWith('url'));
92
- const hasFileExt = typeof val === 'string' && /\.(png|jpe?g|svg|webp|gif|avif|ico|mp4|webm)$/i.test(val.trim());
92
+ const isAssetKey = !isTextKey && (lowerKey.includes('image') || lowerKey.includes('logo') || lowerKey.includes('avatar') || lowerKey.includes('icon') || lowerKey.includes('src') || lowerKey.includes('poster') || lowerKey.includes('thumb'));
93
+ const hasFileExt = typeof val === 'string' && /\.(png|jpe?g|svg|webp|gif|avif|ico|mp4|webm|pdf|woff2?|otf|ttf)$/i.test(val.trim().split('?')[0]);
93
94
 
94
95
  if (typeof val === 'string' && (isAssetKey || hasFileExt)) {
95
96
  checkAssetPath(val, `site-data.json -> ${fieldPath}`);
@@ -104,15 +104,29 @@ function analyzeBlockedProject(targetDir = process.cwd(), options = {}) {
104
104
  let manifestRaw = '';
105
105
 
106
106
  if (!fs.existsSync(manifestPath)) {
107
- findings.push({
108
- ruleId: 'MISSING_MANIFEST',
109
- category: 'FIVORA_CONTRACT',
110
- severity: 'BLOCKING',
111
- file: 'fivora-template.json',
112
- line: 1,
113
- message: 'fivora-template.json is missing in project root.',
114
- remediation: "Run 'npx @deneb-ui/cli init' to generate the mandatory Fivora template manifest.",
115
- });
107
+ if (runReasons.length > 0) {
108
+ for (const reason of runReasons) {
109
+ findings.push({
110
+ ruleId: 'ROLLBACK_TRIGGERED',
111
+ category: 'FIVORA_CONTRACT',
112
+ severity: 'BLOCKING',
113
+ file: 'fivora-template.json',
114
+ line: 1,
115
+ message: `Conversion failed and rolled back: ${reason}`,
116
+ remediation: 'Resolve the contract/asset issues in your template source code, then run init again.',
117
+ });
118
+ }
119
+ } else {
120
+ findings.push({
121
+ ruleId: 'MISSING_MANIFEST',
122
+ category: 'FIVORA_CONTRACT',
123
+ severity: 'BLOCKING',
124
+ file: 'fivora-template.json',
125
+ line: 1,
126
+ message: 'fivora-template.json is missing in project root.',
127
+ remediation: "Run 'npx @deneb-ui/cli init' to generate the mandatory Fivora template manifest.",
128
+ });
129
+ }
116
130
  } else {
117
131
  try {
118
132
  manifestRaw = fs.readFileSync(manifestPath, 'utf8');
package/src/arc/index.cjs CHANGED
@@ -882,7 +882,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
882
882
 
883
883
  const criticalFailure = !syntaxPassed || (contracts.actionCollisions > 0 && appliedCount === 0) || (buildResult.passed === false);
884
884
  const contractFailure = !fivoraAudit.passed || fivoraAudit.uncoveredVisibleText.length > 0;
885
- const designFailure = design.score < 98 && design.total > 0;
885
+ const designFailure = design.score < 95 && design.total > 0;
886
886
  const assetFailure = !assetAudit.passed && assetAudit.errors.length > 0;
887
887
  let outcome = 'success';
888
888
  if (criticalFailure) {
@@ -990,6 +990,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
990
990
  pkg: profile.pkg,
991
991
  },
992
992
  matchedRecipe: recipeMatch.recipe,
993
+ filesChanged: changedFiles,
993
994
  transformedFilesCount: changedFiles.length,
994
995
  totalTransformedElements: appliedCount,
995
996
  totalFields: countSchemaFields(dataBundle.manifest),
@@ -1218,7 +1219,7 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
1218
1219
  if (result.validation?.fivoraContractPassed === false) reasons.push('Fivora strict contract validation failed');
1219
1220
  if (result.validation?.uncoveredVisibleText > 0) reasons.push(`${result.validation.uncoveredVisibleText} uncovered visible text node(s)`);
1220
1221
  if (result.validation?.syntaxPassed === false) reasons.push('AST syntax validation failed');
1221
- if (result.validation?.designPreservation < 98 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
1222
+ if (result.validation?.designPreservation < 95 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
1222
1223
  if (result.validation?.assetIntegrity?.passed === false) reasons.push(`Asset integrity failed: ${result.validation.assetIntegrity.errors?.[0] || 'missing asset'}`);
1223
1224
  if (result.validation?.acceptance && !result.validation.acceptance.passed) reasons.push(`15-Gate Acceptance Matrix failed: ${result.validation.acceptance.status}`);
1224
1225
 
@@ -285,10 +285,9 @@ function collectFieldsFromPlan(plan) {
285
285
  fields.push({ path: t.highlightField, type: 'text', value: t.highlightFallback });
286
286
  }
287
287
  if (t.operation === 'bind-highlighted-heading' && t.field) {
288
- const highlightFrag = (t.fragments || []).find((f) => f.type === 'span' || f.type === 'format');
289
- if (highlightFrag) {
290
- fields.push({ path: `${t.field}Highlight`, type: 'text', value: highlightFrag.text });
291
- }
288
+ const highlightFrag = (t.fragments || []).find((f) => f.type === 'span' || f.type === 'format' || f.type === 'inline-format' || f.tag === 'span');
289
+ const highlightVal = highlightFrag ? (highlightFrag.text || 'Highlight') : (t.highlightFallback || 'Highlight');
290
+ fields.push({ path: `${t.field}Highlight`, type: 'text', value: highlightVal });
292
291
  }
293
292
  if (t.propTransforms) {
294
293
  for (const [propName, propInfo] of Object.entries(t.propTransforms)) {
@@ -87,7 +87,7 @@ function injectDualModeTheme(ast, jsonIdent, providerImport = '@deneb-ui/ui') {
87
87
  if (!hasThemeStyles) {
88
88
  let stylesInjected = false;
89
89
  const snippet = parseSource(
90
- `<ThemeStyles theme={${jsonIdent}?.template?.structure?.theme || ${jsonIdent}?.theme} enableDualMode />`,
90
+ `<ThemeStyles theme={(${jsonIdent} as any)?.template?.structure?.theme || (${jsonIdent} as any)?.theme} enableDualMode />`,
91
91
  'snippet.tsx'
92
92
  );
93
93
  const stylesEl = snippet.program.body[0].expression;
@@ -525,21 +525,201 @@ export function findLinksToUnselectedPages(input: {
525
525
  * the generated-site prefix at runtime.
526
526
  */
527
527
  export function findDoubleBasePathNextRouterCalls(source: string) {
528
- const findings: Array<{ line: number; expression: string }> = [];
529
- const patterns = [
530
- /\b(?:router|navigation)\s*\.\s*(?:push|replace|prefetch)\s*\(\s*withBasePath\s*\(/g,
531
- /<Link\b[^>]{0,500}?\bhref\s*=\s*\{\s*withBasePath\s*\(/g,
532
- ];
533
-
534
- for (const pattern of patterns) {
535
- for (const match of source.matchAll(pattern)) {
536
- const offset = match.index ?? 0;
537
- findings.push({
538
- line: source.slice(0, offset).split(/\r?\n/).length,
539
- expression: match[0].replace(/\s+/g, ' ').trim(),
540
- });
528
+ const findings: Array<{
529
+ line: number;
530
+ expression: string;
531
+ offset: number;
532
+ }> = [];
533
+ const routerNames = new Set(['router', 'navigation']);
534
+ const routerHookNames = new Set(['useRouter']);
535
+ const linkNames = new Set(['Link']);
536
+
537
+ const escapeRegExp = (value: string) =>
538
+ value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
539
+ const compact = (value: string) => {
540
+ const normalized = value.replace(/\s+/g, ' ').trim();
541
+ return normalized.length > 180
542
+ ? `${normalized.slice(0, 177)}...`
543
+ : normalized;
544
+ };
545
+ const readBalanced = (
546
+ start: number,
547
+ opening: '(' | '{',
548
+ closing: ')' | '}',
549
+ ) => {
550
+ let depth = 1;
551
+ let quote: "'" | '"' | '`' | null = null;
552
+ let escaped = false;
553
+ let lineComment = false;
554
+ let blockComment = false;
555
+
556
+ for (let index = start; index < source.length; index += 1) {
557
+ const character = source[index];
558
+ const nextCharacter = source[index + 1];
559
+
560
+ if (lineComment) {
561
+ if (character === '\n') lineComment = false;
562
+ continue;
563
+ }
564
+ if (blockComment) {
565
+ if (character === '*' && nextCharacter === '/') {
566
+ blockComment = false;
567
+ index += 1;
568
+ }
569
+ continue;
570
+ }
571
+ if (quote) {
572
+ if (escaped) {
573
+ escaped = false;
574
+ } else if (character === '\\') {
575
+ escaped = true;
576
+ } else if (character === quote) {
577
+ quote = null;
578
+ }
579
+ continue;
580
+ }
581
+ if (character === '/' && nextCharacter === '/') {
582
+ lineComment = true;
583
+ index += 1;
584
+ continue;
585
+ }
586
+ if (character === '/' && nextCharacter === '*') {
587
+ blockComment = true;
588
+ index += 1;
589
+ continue;
590
+ }
591
+ if (character === "'" || character === '"' || character === '`') {
592
+ quote = character;
593
+ continue;
594
+ }
595
+ if (character === opening) {
596
+ depth += 1;
597
+ } else if (character === closing) {
598
+ depth -= 1;
599
+ if (depth === 0) return source.slice(start, index);
600
+ }
601
+ }
602
+
603
+ return source.slice(start, Math.min(source.length, start + 4_000));
604
+ };
605
+ const containsBasePathReference = (expression: string) => {
606
+ const stripNonCode = (value: string) =>
607
+ value
608
+ .replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g, ' ')
609
+ .replace(/`(?:\\.|[^`\\])*`/g, ' ')
610
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
611
+ .replace(/\/\/[^\r\n]*/g, ' ');
612
+ const templateExpressions = [...expression.matchAll(/\$\{([\s\S]*?)\}/g)]
613
+ .map((match) => stripNonCode(match[1]))
614
+ .join(' ');
615
+ const codeOnly = stripNonCode(expression);
616
+ return /\b(?:[A-Za-z_$][\w$]*\s*\.\s*)*[A-Za-z_$][\w$]*(?:base_?path|path_?base)[\w$]*\b/i.test(
617
+ `${codeOnly} ${templateExpressions}`,
618
+ );
619
+ };
620
+ const addFinding = (offset: number, expression: string) => {
621
+ findings.push({
622
+ line: source.slice(0, offset).split(/\r?\n/).length,
623
+ expression: compact(expression),
624
+ offset,
625
+ });
626
+ };
627
+
628
+ for (const match of source.matchAll(
629
+ /\bimport\s*\{([^}]*)\}\s*from\s*['"]next\/(?:navigation|router)['"]/g,
630
+ )) {
631
+ for (const importedName of match[1].split(',')) {
632
+ const alias = importedName
633
+ .trim()
634
+ .match(/^useRouter(?:\s+as\s+([A-Za-z_$][\w$]*))?$/)?.[1];
635
+ if (alias) routerHookNames.add(alias);
541
636
  }
542
637
  }
543
638
 
544
- return findings;
639
+ const hookAlternation = [...routerHookNames].map(escapeRegExp).join('|');
640
+ const routerAssignmentPattern = new RegExp(
641
+ `\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=;]+)?=\\s*(?:${hookAlternation})\\s*\\(`,
642
+ 'g',
643
+ );
644
+ for (const match of source.matchAll(routerAssignmentPattern)) {
645
+ routerNames.add(match[1]);
646
+ }
647
+
648
+ for (const match of source.matchAll(
649
+ /\bimport\s+([^;]{0,300}?)\s+from\s*['"]next\/link['"]/g,
650
+ )) {
651
+ const importClause = match[1].trim();
652
+ const defaultImport = importClause.match(/^([A-Za-z_$][\w$]*)/i)?.[1];
653
+ if (defaultImport && defaultImport !== 'type') linkNames.add(defaultImport);
654
+ for (const namedImport of importClause.matchAll(
655
+ /\bdefault\s+as\s+([A-Za-z_$][\w$]*)/g,
656
+ )) {
657
+ linkNames.add(namedImport[1]);
658
+ }
659
+ }
660
+
661
+ for (const match of source.matchAll(
662
+ /\b([A-Za-z_$][\w$]*)\s*\.\s*(push|replace|prefetch)\s*\(/g,
663
+ )) {
664
+ const routerName = match[1];
665
+ if (
666
+ !routerNames.has(routerName) &&
667
+ !/(?:router|navigation)/i.test(routerName)
668
+ ) {
669
+ continue;
670
+ }
671
+ const destination = readBalanced(
672
+ (match.index ?? 0) + match[0].length,
673
+ '(',
674
+ ')',
675
+ );
676
+ if (containsBasePathReference(destination)) {
677
+ addFinding(
678
+ match.index ?? 0,
679
+ `${routerName}.${match[2]}(${destination})`,
680
+ );
681
+ }
682
+ }
683
+
684
+ const directHookCallPattern = new RegExp(
685
+ `\\b(?:${hookAlternation})\\s*\\(\\s*\\)\\s*\\.\\s*(push|replace|prefetch)\\s*\\(`,
686
+ 'g',
687
+ );
688
+ for (const match of source.matchAll(directHookCallPattern)) {
689
+ const destination = readBalanced(
690
+ (match.index ?? 0) + match[0].length,
691
+ '(',
692
+ ')',
693
+ );
694
+ if (containsBasePathReference(destination)) {
695
+ addFinding(match.index ?? 0, `${match[0]}${destination})`);
696
+ }
697
+ }
698
+
699
+ const linkAlternation = [...linkNames].map(escapeRegExp).join('|');
700
+ const linkHrefPattern = new RegExp(
701
+ `<(${linkAlternation})\\b[^>]{0,2000}?\\bhref\\s*=\\s*\\{`,
702
+ 'g',
703
+ );
704
+ for (const match of source.matchAll(linkHrefPattern)) {
705
+ const destination = readBalanced(
706
+ (match.index ?? 0) + match[0].length,
707
+ '{',
708
+ '}',
709
+ );
710
+ if (containsBasePathReference(destination)) {
711
+ addFinding(
712
+ match.index ?? 0,
713
+ `<${match[1]} href={${destination}}>`,
714
+ );
715
+ }
716
+ }
717
+
718
+ return findings
719
+ .sort((left, right) => left.offset - right.offset)
720
+ .filter(
721
+ (finding, index, ordered) =>
722
+ index === 0 || finding.offset !== ordered[index - 1].offset,
723
+ )
724
+ .map(({ line, expression }) => ({ line, expression }));
545
725
  }