@deneb-ui/cli 2.0.103 → 2.0.105
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 +7 -1
- package/package.json +2 -2
- package/src/arc/acceptance-gates.cjs +1 -1
- package/src/arc/fivora-contract.cjs +11 -1
- package/src/arc/index.cjs +30 -6
- package/src/arc/manifest.cjs +58 -1
- package/src/arc/pr-agent.cjs +8 -3
- package/src/arc/semantic.cjs +15 -0
- package/src/arc/transforms/healing/conditionals.cjs +21 -41
- package/src/arc/validator.cjs +37 -12
package/bin/index.js
CHANGED
|
@@ -929,7 +929,13 @@ async function initProject(targetInput, options = {}) {
|
|
|
929
929
|
conversionRes = pipelineRes.result || pipelineRes;
|
|
930
930
|
if (pipelineRes.rolledBack) {
|
|
931
931
|
console.log('\n\x1b[31m✖ DENEB CONVERSION BLOCKED — Original project files preserved intact.\x1b[0m');
|
|
932
|
-
|
|
932
|
+
const primaryReason = pipelineRes.reasons?.[0] || pipelineRes.error || '100% Fivora Contract / Acceptance Gate validation failed';
|
|
933
|
+
console.log(`\x1b[33m Reason:\x1b[0m ${primaryReason}`);
|
|
934
|
+
if (pipelineRes.reasons && pipelineRes.reasons.length > 1) {
|
|
935
|
+
for (let i = 1; i < pipelineRes.reasons.length; i++) {
|
|
936
|
+
console.log(`\x1b[90m - ${pipelineRes.reasons[i]}\x1b[0m`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
933
939
|
console.log(`\n Run \x1b[36m\x1b[1mnpx @deneb-ui/cli explain --blocked\x1b[0m for exact file locations, line numbers, and fixes.\n`);
|
|
934
940
|
process.exit(1);
|
|
935
941
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.105",
|
|
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.105",
|
|
53
53
|
"@octokit/rest": "^22.0.1",
|
|
54
54
|
"adm-zip": "^0.6.0",
|
|
55
55
|
"dotenv": "^17.4.2",
|
|
@@ -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) >= 90),
|
|
87
87
|
critical: false,
|
|
88
88
|
},
|
|
89
89
|
controlOnlyProtection: {
|
|
@@ -615,16 +615,26 @@ function findUncoveredVisibleTextRegex(code, filePath) {
|
|
|
615
615
|
function auditEmptyStateSource(code, filePath) {
|
|
616
616
|
const errors = [];
|
|
617
617
|
const source = String(code);
|
|
618
|
+
const isInteractiveOrPageToggle = (condition) => {
|
|
619
|
+
return /(?:open|active|selected|modal|dialog|lightbox|drawer|popup|sheet|portal|loading|pending|tab|filter|step|enabled|ispageenabled|haspage|pagestatus|pageenabled)/i.test(
|
|
620
|
+
String(condition || '')
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
|
|
618
624
|
const andPattern =
|
|
619
625
|
/\{([^{}]{0,120}?)\s*&&\s*\(?\s*<\s*([a-zA-Z][\w.:-]*)([^>]*data-preview-(?:field-path|list-path|item-path)[^>]*)>/g;
|
|
620
626
|
for (const match of source.matchAll(andPattern)) {
|
|
627
|
+
const condition = String(match[1]).trim();
|
|
628
|
+
if (isInteractiveOrPageToggle(condition)) continue;
|
|
621
629
|
errors.push(
|
|
622
|
-
`${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker is gated behind "${
|
|
630
|
+
`${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker is gated behind "${condition} &&". Keep the target mounted when its value is empty, false, or zero.`
|
|
623
631
|
);
|
|
624
632
|
}
|
|
625
633
|
const ternaryPattern =
|
|
626
634
|
/\{([^{}]{0,80}?)\s*\?\s*<\s*([a-zA-Z][\w.:-]*)([^>]*data-preview-(?:field-path|list-path|item-path)[^>]*)>[\s\S]{0,200}?\?\s*(?:null|false|undefined)/g;
|
|
627
635
|
for (const match of source.matchAll(ternaryPattern)) {
|
|
636
|
+
const condition = String(match[1]).trim();
|
|
637
|
+
if (isInteractiveOrPageToggle(condition)) continue;
|
|
628
638
|
errors.push(
|
|
629
639
|
`${filePath}:${lineNumberAt(source, match.index ?? 0)} editable marker unmounts on a falsy ternary. Keep the target mounted when its value is empty.`
|
|
630
640
|
);
|
package/src/arc/index.cjs
CHANGED
|
@@ -21,7 +21,8 @@ const { planTransformations } = require('./planner.cjs');
|
|
|
21
21
|
const { applyFilePlan, instrumentLayoutSource, instrumentPageKey, resolveSiteDataSpecifier, resolveSiteDataRuntimeSpecifier, rewriteRecursiveSiteDataContext, ensureJsonModule, sanitizeContradictoryMarkersInSource } = require('./transformer.cjs');
|
|
22
22
|
const { applyResidualPass } = require('./residual.cjs');
|
|
23
23
|
const { parseSource } = require('./ast.cjs');
|
|
24
|
-
const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields } = require('./manifest.cjs');
|
|
24
|
+
const { buildSiteDataAndManifest, writeDataBank, loadExistingData, countSchemaFields, upsertSchemaField, getDeep } = require('./manifest.cjs');
|
|
25
|
+
const { classifyFieldType, humanLabel } = require('./field-paths.cjs');
|
|
25
26
|
const { collectFontIdsFromSiteData } = require('./font-plan.cjs');
|
|
26
27
|
const { validateAstFiles, validateContracts, designPreservationScore, coverageMetrics } = require('./validator.cjs');
|
|
27
28
|
const { recordExperience, registryArchitecture } = require('./learning.cjs');
|
|
@@ -751,7 +752,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
751
752
|
.map((f) => ({ abs: path.join(projectDir, f), rel: f }))
|
|
752
753
|
);
|
|
753
754
|
const syntaxPassed = astResults.every((r) => r.passed);
|
|
754
|
-
|
|
755
|
+
let contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
755
756
|
const design = designPreservationScore(plan.files, afterFiles);
|
|
756
757
|
const alreadyEditable = analyses.filter((a) => a.alreadyEditable).length;
|
|
757
758
|
const skippedDynamic = plan.skipped.filter((s) => /dynamic|api/.test(s.reason || '')).length;
|
|
@@ -786,6 +787,26 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
786
787
|
manifest: dataBundle.manifest,
|
|
787
788
|
inventory: collectMarkerInventory(projectDir, profile, graph),
|
|
788
789
|
});
|
|
790
|
+
contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Auto-reconciliation: If contracts flagged missingSchema, register them in manifest.editorSchema
|
|
795
|
+
if (contracts.missingSchema.length > 0) {
|
|
796
|
+
let schemaModified = false;
|
|
797
|
+
dataBundle.manifest.editorSchema = dataBundle.manifest.editorSchema || { version: 1, sections: [] };
|
|
798
|
+
dataBundle.manifest.editorSchema.sections = dataBundle.manifest.editorSchema.sections || [];
|
|
799
|
+
for (const item of contracts.missingSchema) {
|
|
800
|
+
const fp = item.fieldPath;
|
|
801
|
+
const leaf = fp.split('.').pop();
|
|
802
|
+
const val = getDeep(dataBundle.siteData.content, fp);
|
|
803
|
+
const type = classifyFieldType('text', val, leaf);
|
|
804
|
+
upsertSchemaField(dataBundle.manifest.editorSchema.sections, fp, type, humanLabel(leaf));
|
|
805
|
+
schemaModified = true;
|
|
806
|
+
}
|
|
807
|
+
if (schemaModified) {
|
|
808
|
+
writeDataBank(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
809
|
+
contracts = validateContracts(projectDir, dataBundle.siteData, dataBundle.manifest);
|
|
789
810
|
}
|
|
790
811
|
}
|
|
791
812
|
|
|
@@ -882,7 +903,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
882
903
|
|
|
883
904
|
const criticalFailure = !syntaxPassed || (contracts.actionCollisions > 0 && appliedCount === 0) || (buildResult.passed === false);
|
|
884
905
|
const contractFailure = !fivoraAudit.passed || fivoraAudit.uncoveredVisibleText.length > 0;
|
|
885
|
-
const designFailure = design.score <
|
|
906
|
+
const designFailure = design.score < 90 && design.total > 0;
|
|
886
907
|
const assetFailure = !assetAudit.passed && assetAudit.errors.length > 0;
|
|
887
908
|
let outcome = 'success';
|
|
888
909
|
if (criticalFailure) {
|
|
@@ -909,7 +930,7 @@ function runArcTransformations(projectDir, projectName, opts, profile, graph, an
|
|
|
909
930
|
const reasons = [];
|
|
910
931
|
if (!fivoraAudit.passed) reasons.push(`${fivoraAudit.errors.length} Fivora contract error(s)`);
|
|
911
932
|
if (fivoraAudit.uncoveredVisibleText.length > 0) reasons.push(`${fivoraAudit.uncoveredVisibleText.length} uncovered visible text node(s)`);
|
|
912
|
-
if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} <
|
|
933
|
+
if (designFailure) reasons.push(`Design preservation score below threshold (${design.score} < 90)`);
|
|
913
934
|
if (assetFailure) reasons.push(`Asset integrity failed (${assetAudit.errors.length} missing asset(s))`);
|
|
914
935
|
if (!acceptance.allCriticalPassed) reasons.push(`Critical acceptance gates failed: ${acceptance.status}`);
|
|
915
936
|
printer.printRollback(`Strict Fivora contract failed: ${reasons.join(', ')}`);
|
|
@@ -1180,7 +1201,9 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
|
|
|
1180
1201
|
const syntaxOk = result.validation?.syntaxPassed !== false;
|
|
1181
1202
|
const assetOk = result.validation?.assetIntegrity?.passed !== false;
|
|
1182
1203
|
const gatesOk = result.validation?.acceptance ? result.validation.acceptance.passed : true;
|
|
1183
|
-
const isSuccess =
|
|
1204
|
+
const isSuccess =
|
|
1205
|
+
result.outcome !== 'rolled-back' &&
|
|
1206
|
+
(!opts.strict || (result.outcome === 'success' && contractOk && syntaxOk && assetOk && gatesOk));
|
|
1184
1207
|
|
|
1185
1208
|
if (isSuccess) {
|
|
1186
1209
|
// Collect all files to commit from workspace to project
|
|
@@ -1219,7 +1242,7 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
|
|
|
1219
1242
|
if (result.validation?.fivoraContractPassed === false) reasons.push('Fivora strict contract validation failed');
|
|
1220
1243
|
if (result.validation?.uncoveredVisibleText > 0) reasons.push(`${result.validation.uncoveredVisibleText} uncovered visible text node(s)`);
|
|
1221
1244
|
if (result.validation?.syntaxPassed === false) reasons.push('AST syntax validation failed');
|
|
1222
|
-
if (result.validation?.designPreservation <
|
|
1245
|
+
if (result.validation?.designPreservation < 90 && result.validation?.designPreservation > 0) reasons.push(`Design preservation (${result.validation.designPreservation}%) below threshold`);
|
|
1223
1246
|
if (result.validation?.assetIntegrity?.passed === false) reasons.push(`Asset integrity failed: ${result.validation.assetIntegrity.errors?.[0] || 'missing asset'}`);
|
|
1224
1247
|
if (result.validation?.acceptance && !result.validation.acceptance.passed) reasons.push(`15-Gate Acceptance Matrix failed: ${result.validation.acceptance.status}`);
|
|
1225
1248
|
|
|
@@ -1251,6 +1274,7 @@ function runTransactionalPipeline(targetDirInput = '.', options = {}) {
|
|
|
1251
1274
|
rolledBack: true,
|
|
1252
1275
|
outcome: 'rolled-back',
|
|
1253
1276
|
error: err.message,
|
|
1277
|
+
reasons: [err.message],
|
|
1254
1278
|
};
|
|
1255
1279
|
}
|
|
1256
1280
|
}
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -180,7 +180,31 @@ function upsertSchemaList(sections, listPath, itemFields, items) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
const key = rest[rest.length - 1];
|
|
183
|
-
|
|
183
|
+
const existingList = fields.find((f) => f.key === key);
|
|
184
|
+
if (existingList) {
|
|
185
|
+
existingList.fields = existingList.fields || [];
|
|
186
|
+
for (const field of itemFields || []) {
|
|
187
|
+
if (!existingList.fields.some((ef) => ef.key === field.key)) {
|
|
188
|
+
existingList.fields.push({
|
|
189
|
+
key: field.key,
|
|
190
|
+
type:
|
|
191
|
+
field.type === 'url'
|
|
192
|
+
? 'url'
|
|
193
|
+
: field.type === 'image'
|
|
194
|
+
? 'image'
|
|
195
|
+
: field.type === 'textarea'
|
|
196
|
+
? 'textarea'
|
|
197
|
+
: field.type === 'tel' || field.type === 'phone'
|
|
198
|
+
? 'tel'
|
|
199
|
+
: field.type === 'email'
|
|
200
|
+
? 'email'
|
|
201
|
+
: field.type || 'text',
|
|
202
|
+
label: humanLabel(field.key),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
184
208
|
|
|
185
209
|
const isPrimitiveList = (itemFields || []).length === 0;
|
|
186
210
|
fields.push({
|
|
@@ -255,6 +279,9 @@ function enrichSchemasFromContent(content, sections) {
|
|
|
255
279
|
if (Object.prototype.hasOwnProperty.call(node, labelKey)) {
|
|
256
280
|
upsertSchemaField(sections, `${prefix}.${labelKey}`, 'text', humanLabel(labelKey));
|
|
257
281
|
}
|
|
282
|
+
} else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
283
|
+
const fieldType = classifyFieldType('text', value, key);
|
|
284
|
+
upsertSchemaField(sections, nextPath, fieldType, humanLabel(key));
|
|
258
285
|
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
259
286
|
walk(value, nextPath);
|
|
260
287
|
}
|
|
@@ -627,6 +654,35 @@ function buildSiteDataAndManifest({
|
|
|
627
654
|
}
|
|
628
655
|
}
|
|
629
656
|
|
|
657
|
+
// Ensure all bound field paths (e.g., from existing template code) are registered in editorSchema and content
|
|
658
|
+
for (const bPath of boundFieldPaths || []) {
|
|
659
|
+
if (!bPath || typeof bPath !== 'string') continue;
|
|
660
|
+
const listMatch = bPath.match(/^(.*?)\[(?:\d+|\*|[a-zA-Z0-9_]+)\]\.(.*)$/);
|
|
661
|
+
if (listMatch) {
|
|
662
|
+
const listPath = listMatch[1];
|
|
663
|
+
const itemKey = listMatch[2];
|
|
664
|
+
const listVal = getDeep(content, listPath) || [];
|
|
665
|
+
const sample = Array.isArray(listVal) && listVal[0] ? listVal[0] : {};
|
|
666
|
+
const itemVal = getDeep(sample, itemKey);
|
|
667
|
+
const itemType = classifyFieldType('text', itemVal, itemKey);
|
|
668
|
+
upsertSchemaList(editorSections, listPath, [{ key: itemKey, type: itemType }], listVal);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (!isBound(bPath) && !isAllowedControlOnly(bPath)) continue;
|
|
672
|
+
let val = getDeep(content, bPath);
|
|
673
|
+
if (val === undefined && existingSiteData?.content) {
|
|
674
|
+
val = getDeep(existingSiteData.content, bPath);
|
|
675
|
+
if (val !== undefined) setDeep(content, bPath, val);
|
|
676
|
+
}
|
|
677
|
+
if (val === undefined) {
|
|
678
|
+
setDeep(content, bPath, '');
|
|
679
|
+
val = '';
|
|
680
|
+
}
|
|
681
|
+
const leafKey = bPath.split('.').pop();
|
|
682
|
+
const type = classifyFieldType('text', val, leafKey);
|
|
683
|
+
upsertSchemaField(editorSections, bPath, type, humanLabel(leafKey));
|
|
684
|
+
}
|
|
685
|
+
|
|
630
686
|
pruneUnboundLeaves(content, isBound);
|
|
631
687
|
|
|
632
688
|
const siteData = {
|
|
@@ -747,6 +803,7 @@ module.exports = {
|
|
|
747
803
|
collectFieldsFromPlan,
|
|
748
804
|
computeControlOnlyPaths,
|
|
749
805
|
assignSectionPageKeys,
|
|
806
|
+
upsertSchemaField,
|
|
750
807
|
upsertSchemaList,
|
|
751
808
|
enrichSchemasFromContent,
|
|
752
809
|
isListActionCtaKey,
|
package/src/arc/pr-agent.cjs
CHANGED
|
@@ -32,6 +32,11 @@ function getGithubConfig() {
|
|
|
32
32
|
* @returns {{ ready: boolean, reason?: string }}
|
|
33
33
|
*/
|
|
34
34
|
function checkGithubReady() {
|
|
35
|
+
// Automated branch/PR creation is permanently disabled by default.
|
|
36
|
+
// Set DENEB_ENABLE_PR=true only if you explicitly want to create GitHub branches.
|
|
37
|
+
if (process.env.DENEB_ENABLE_PR !== 'true') {
|
|
38
|
+
return { ready: false, reason: 'Automated GitHub branch creation is disabled.' };
|
|
39
|
+
}
|
|
35
40
|
loadEnv();
|
|
36
41
|
const config = getGithubConfig();
|
|
37
42
|
if (!config.pat) {
|
|
@@ -204,10 +209,10 @@ async function createComponentPR(params) {
|
|
|
204
209
|
let corePr = null;
|
|
205
210
|
let uiPr = null;
|
|
206
211
|
|
|
207
|
-
if (dryRun) {
|
|
212
|
+
if (dryRun || process.env.DENEB_ENABLE_PR !== 'true') {
|
|
208
213
|
return {
|
|
209
|
-
corePr: { number: 0, url: '(
|
|
210
|
-
uiPr: docsCode ? { number: 0, url: '(
|
|
214
|
+
corePr: { number: 0, url: '(branch/PR creation disabled)' },
|
|
215
|
+
uiPr: docsCode ? { number: 0, url: '(branch/PR creation disabled)' } : null,
|
|
211
216
|
errors: [],
|
|
212
217
|
};
|
|
213
218
|
}
|
package/src/arc/semantic.cjs
CHANGED
|
@@ -595,6 +595,21 @@ function analyzeFile(optionsOrFile, codeArg, profileArg) {
|
|
|
595
595
|
return;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
if (hasStaticMarker(node)) {
|
|
599
|
+
candidates.push({
|
|
600
|
+
loc,
|
|
601
|
+
tag: name,
|
|
602
|
+
kind: 'static',
|
|
603
|
+
confidence: 1,
|
|
604
|
+
skip: true,
|
|
605
|
+
reason: 'already-static-marker',
|
|
606
|
+
file: relativeFile,
|
|
607
|
+
ownerScope,
|
|
608
|
+
});
|
|
609
|
+
this.traverse(pathNode);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
598
613
|
if (isElementOrParentHidden(pathNode)) {
|
|
599
614
|
candidates.push({
|
|
600
615
|
loc,
|
|
@@ -31,47 +31,23 @@ function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
|
|
|
31
31
|
|
|
32
32
|
if (leftIdentifiers.size === 0) return true;
|
|
33
33
|
|
|
34
|
-
// 2.
|
|
35
|
-
const
|
|
34
|
+
// 2. Interactive state & modal variable keywords (e.g. lightboxIndex, selectedDish, isOpen, activeTab, isModalOpen)
|
|
35
|
+
const interactiveStateKeywordRegex = /(?:modal|dialog|lightbox|drawer|popup|sheet|portal|isopen|activetab|selecteditem|selecteddish|lightboxindex|expanded|loading|pending)/i;
|
|
36
36
|
for (const id of leftIdentifiers) {
|
|
37
|
-
if (
|
|
38
|
-
return true; // NEVER strip modal/state
|
|
37
|
+
if (interactiveStateKeywordRegex.test(id)) {
|
|
38
|
+
return true; // NEVER strip modal/interactive state guards
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// 3.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
this.traverse(bPath);
|
|
51
|
-
},
|
|
52
|
-
visitUnaryExpression(uPath) {
|
|
53
|
-
if (uPath.node.operator === '!') {
|
|
54
|
-
hasComparisonOrNullCheck = true;
|
|
55
|
-
}
|
|
56
|
-
this.traverse(uPath);
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
if (hasComparisonOrNullCheck) return true;
|
|
60
|
-
|
|
61
|
-
// 4. Check if ANY identifier in leftExpr is referenced inside rightExpr
|
|
62
|
-
let rightReferencesLeftId = false;
|
|
63
|
-
recast.types.visit(rightExpr, {
|
|
64
|
-
visitIdentifier(idPath) {
|
|
65
|
-
if (leftIdentifiers.has(idPath.node.name)) {
|
|
66
|
-
rightReferencesLeftId = true;
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
this.traverse(idPath);
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
if (rightReferencesLeftId) return true;
|
|
42
|
+
// 3. Page & feature toggles (e.g. inventoryEnabled, contactEnabled, isPageEnabled)
|
|
43
|
+
const pageToggleRegex = /(?:enabled|ispageenabled|haspage|pagestatus|pageenabled)/i;
|
|
44
|
+
for (const id of leftIdentifiers) {
|
|
45
|
+
if (pageToggleRegex.test(id)) {
|
|
46
|
+
return true; // NEVER strip page/route enablement guards
|
|
47
|
+
}
|
|
48
|
+
}
|
|
73
49
|
|
|
74
|
-
//
|
|
50
|
+
// 4. Check if rightExpr or any of its ancestors is an interactive modal / dialog / overlay / AnimatePresence
|
|
75
51
|
let curr = pathNode;
|
|
76
52
|
while (curr && curr.parent) {
|
|
77
53
|
const parentNode = curr.parent.node;
|
|
@@ -111,11 +87,6 @@ function isUnsafeToStripCondition(leftExpr, rightExpr, pathNode) {
|
|
|
111
87
|
});
|
|
112
88
|
if (isModalOrOverlay) return true;
|
|
113
89
|
|
|
114
|
-
// 6. Only allow stripping if leftExpr is purely a direct siteData content member chain
|
|
115
|
-
const leftCode = recast.print(leftExpr).code;
|
|
116
|
-
const isDirectContentCheck = /siteData(?:\?\.|\.)content(?:\?\.|\.)/i.test(leftCode);
|
|
117
|
-
if (!isDirectContentCheck) return true;
|
|
118
|
-
|
|
119
90
|
return false;
|
|
120
91
|
}
|
|
121
92
|
|
|
@@ -141,6 +112,15 @@ function healEmptyStateConditionals(ast) {
|
|
|
141
112
|
) {
|
|
142
113
|
return true;
|
|
143
114
|
}
|
|
115
|
+
const attrs = node.openingElement?.attributes || [];
|
|
116
|
+
for (const a of attrs) {
|
|
117
|
+
if (a.type === 'JSXSpreadAttribute') {
|
|
118
|
+
const code = recast.print(a).code;
|
|
119
|
+
if (/data-preview-(?:field-path|list-path|item-path)/.test(code)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
144
124
|
return (node.children || []).some((child) => jsxHasPreviewMarker(child));
|
|
145
125
|
}
|
|
146
126
|
if (node.type === 'JSXFragment') {
|
package/src/arc/validator.cjs
CHANGED
|
@@ -48,7 +48,7 @@ function validateContracts(projectDir, siteData, manifest) {
|
|
|
48
48
|
if (siteData?.content && getDeep(siteData.content, fieldPath) === undefined) {
|
|
49
49
|
orphans.push({ fieldPath, file: relative });
|
|
50
50
|
}
|
|
51
|
-
if (manifest && !isFieldInEditorSchema(manifest, fieldPath)) {
|
|
51
|
+
if (manifest && !isFieldInEditorSchema(manifest, fieldPath) && !isControlOnly(fieldPath, manifest.visualEditing?.controlOnlyPaths)) {
|
|
52
52
|
missingSchema.push({ fieldPath, file: relative });
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -82,24 +82,49 @@ function validateContracts(projectDir, siteData, manifest) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function isFieldInEditorSchema(manifest, fieldPath) {
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (!manifest?.editorSchema?.sections) return false;
|
|
86
|
+
const cleanPath = String(fieldPath)
|
|
87
|
+
.replace(/\[(?:\d+|\*|[a-zA-Z0-9_]+)\]/g, '.[*]')
|
|
88
|
+
.replace(/\.\.+/g, '.');
|
|
89
|
+
const parts = cleanPath.split('.').filter(Boolean);
|
|
90
|
+
const rawSectionId = parts[0];
|
|
91
|
+
const sectionId = rawSectionId ? rawSectionId.replace(/\[\*\]/g, '') : '';
|
|
92
|
+
|
|
93
|
+
const section = (manifest.editorSchema?.sections || []).find(
|
|
94
|
+
(s) => s.id === sectionId || s.path === sectionId || s.id === rawSectionId
|
|
95
|
+
);
|
|
89
96
|
if (!section) return false;
|
|
97
|
+
const fieldParts = parts.slice(1);
|
|
98
|
+
if (fieldParts.length === 0) return true;
|
|
99
|
+
|
|
100
|
+
function check(fields, remainingParts) {
|
|
101
|
+
if (!Array.isArray(fields) || remainingParts.length === 0) return false;
|
|
102
|
+
const current = remainingParts[0];
|
|
103
|
+
const rest = remainingParts.slice(1);
|
|
104
|
+
|
|
105
|
+
if (current === '[*]') {
|
|
106
|
+
if (rest.length === 0) return true;
|
|
107
|
+
return check(fields, rest);
|
|
108
|
+
}
|
|
90
109
|
|
|
91
|
-
function checkFields(fields, targetKey) {
|
|
92
|
-
if (!Array.isArray(fields)) return false;
|
|
93
110
|
for (const f of fields) {
|
|
94
|
-
if (f.key ===
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
111
|
+
if (f.key === current) {
|
|
112
|
+
if (rest.length === 0) return true;
|
|
113
|
+
const subFields = f.fields || f.itemFields;
|
|
114
|
+
if (subFields) {
|
|
115
|
+
if (rest[0] === '[*]') {
|
|
116
|
+
if (rest.length === 1) return true;
|
|
117
|
+
if (check(subFields, rest.slice(1))) return true;
|
|
118
|
+
}
|
|
119
|
+
if (check(subFields, rest)) return true;
|
|
120
|
+
}
|
|
98
121
|
}
|
|
122
|
+
if (f.key === remainingParts.join('.')) return true;
|
|
99
123
|
}
|
|
100
124
|
return false;
|
|
101
125
|
}
|
|
102
|
-
|
|
126
|
+
|
|
127
|
+
return check(section.fields || section.itemFields, fieldParts);
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
function designPreservationScore(filePlans, afterFiles) {
|