@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 +2 -2
- package/src/arc/__tests__/arc.test.cjs +4 -3
- package/src/arc/acceptance-gates.cjs +1 -1
- package/src/arc/asset-auditor.cjs +6 -5
- package/src/arc/explain-blocked.cjs +23 -9
- package/src/arc/index.cjs +3 -2
- package/src/arc/manifest.cjs +3 -4
- package/src/arc/transforms/routing/shared-layout.cjs +1 -1
- package/src/sites/universal-page-selection.ts +194 -14
- package/src/tools/deneb-template-validator.cjs +14 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
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
|
-
"
|
|
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:
|
|
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) >=
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 <
|
|
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 <
|
|
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
|
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -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
|
-
|
|
290
|
-
|
|
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<{
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
];
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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
|
}
|