@deneb-ui/cli 2.0.87 → 2.0.89
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 +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/tools/deneb-template-validator.cjs +17 -17
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const {
|
|
6
|
+
STANDARD_VIEWPORTS,
|
|
7
|
+
calculateVisualPreservation,
|
|
8
|
+
extractDesignSnapshot,
|
|
9
|
+
} = require('../visual-regression.cjs');
|
|
10
|
+
|
|
11
|
+
test('Visual Regression: extracts element tags, layout classes, and compound icons', () => {
|
|
12
|
+
const code = `
|
|
13
|
+
export function Hero() {
|
|
14
|
+
return (
|
|
15
|
+
<section className="relative flex flex-col md:grid md:grid-cols-2 gap-8 p-12 bg-white">
|
|
16
|
+
<h1 className="text-4xl font-bold">Original Title</h1>
|
|
17
|
+
<button className="btn flex items-center gap-2">
|
|
18
|
+
<svg className="w-5 h-5" />
|
|
19
|
+
<span>Order Now</span>
|
|
20
|
+
</button>
|
|
21
|
+
</section>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const snapshot = extractDesignSnapshot(code);
|
|
27
|
+
assert.ok(snapshot.tags.includes('section'));
|
|
28
|
+
assert.ok(snapshot.tags.includes('h1'));
|
|
29
|
+
assert.ok(snapshot.tags.includes('button'));
|
|
30
|
+
assert.ok(snapshot.classes.has('flex'));
|
|
31
|
+
assert.ok(snapshot.classes.has('md:grid-cols-2'));
|
|
32
|
+
|
|
33
|
+
const btnElem = snapshot.elements.find((e) => e.tag === 'button');
|
|
34
|
+
assert.ok(btnElem);
|
|
35
|
+
assert.equal(btnElem.hasSvg, true);
|
|
36
|
+
assert.equal(btnElem.hasSpan, true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('Visual Regression: calculates >= 98% design preservation on valid ARC transformation', () => {
|
|
40
|
+
const beforeCode = `
|
|
41
|
+
export function Banner() {
|
|
42
|
+
return (
|
|
43
|
+
<div className="grid grid-cols-3 gap-6 p-8">
|
|
44
|
+
<h2>Featured Items</h2>
|
|
45
|
+
<p>Hand-picked for you</p>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
// Transformed code has siteData getters and preview marker, but identical layout classes & tags
|
|
52
|
+
const afterCode = `
|
|
53
|
+
export function Banner() {
|
|
54
|
+
return (
|
|
55
|
+
<div className="grid grid-cols-3 gap-6 p-8">
|
|
56
|
+
<h2 data-preview-field-path="home.banner.title">{siteData?.content?.home?.banner?.title ?? "Featured Items"}</h2>
|
|
57
|
+
<p data-preview-field-path="home.banner.subtitle">{siteData?.content?.home?.banner?.subtitle ?? "Hand-picked for you"}</p>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const report = calculateVisualPreservation(beforeCode, afterCode);
|
|
64
|
+
assert.equal(report.passed, true);
|
|
65
|
+
assert.ok(report.overallPreservationScore >= 98.0);
|
|
66
|
+
assert.ok(report.viewports.mobile.preservationScore >= 98.0);
|
|
67
|
+
assert.ok(report.viewports.tablet.preservationScore >= 98.0);
|
|
68
|
+
assert.ok(report.viewports.desktop.preservationScore >= 98.0);
|
|
69
|
+
assert.equal(report.blockingIssues.length, 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Visual Regression: detects dropped layout classes or icons and reports blocking issue', () => {
|
|
73
|
+
const beforeCode = `
|
|
74
|
+
export function Action() {
|
|
75
|
+
return (
|
|
76
|
+
<button className="btn">
|
|
77
|
+
<svg className="icon" />
|
|
78
|
+
<span>Checkout</span>
|
|
79
|
+
</button>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
// Broken transform accidentally removed the svg icon
|
|
85
|
+
const brokenCode = `
|
|
86
|
+
export function Action() {
|
|
87
|
+
return (
|
|
88
|
+
<button className="btn">
|
|
89
|
+
<span>Checkout</span>
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const report = calculateVisualPreservation(beforeCode, brokenCode);
|
|
96
|
+
assert.equal(report.passed, false);
|
|
97
|
+
assert.ok(report.blockingIssues.length > 0);
|
|
98
|
+
assert.ok(report.blockingIssues[0].includes('SVG or icon dropped'));
|
|
99
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC v3 — Production Acceptance Gates Engine
|
|
5
|
+
* Implements the 12-Gate Acceptance Matrix recommended for enterprise Next.js -> Fivora compilation.
|
|
6
|
+
*/
|
|
7
|
+
const ACCEPTANCE_GATES = {
|
|
8
|
+
SOURCE_ANALYSIS: 'Source Analysis',
|
|
9
|
+
AST_TRANSFORMATION: 'AST Transformation',
|
|
10
|
+
TYPESCRIPT_VALIDATION: 'TypeScript / JS Validation',
|
|
11
|
+
BUILD_SUCCESS: 'Next.js Build',
|
|
12
|
+
FIVORA_CONTRACT: 'Fivora Contract',
|
|
13
|
+
MANIFEST_SITE_DATA: 'Manifest / Site Data',
|
|
14
|
+
RUNTIME_DATA_BINDING: 'Runtime Data Binding',
|
|
15
|
+
RUNTIME_COLLECTION_OPS: 'Runtime Collection Operations',
|
|
16
|
+
RUNTIME_IMAGE_EDITING: 'Runtime Image Editing',
|
|
17
|
+
ROUTE_COVERAGE: 'Route Coverage',
|
|
18
|
+
DESIGN_PRESERVATION: 'Design Preservation',
|
|
19
|
+
CONTROL_ONLY_PROTECTION: 'Control-Only Protection',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Evaluates compilation metrics against the 12-Gate Acceptance Matrix.
|
|
24
|
+
*/
|
|
25
|
+
function evaluateAcceptanceGates(metrics = {}) {
|
|
26
|
+
const gates = {
|
|
27
|
+
sourceAnalysis: {
|
|
28
|
+
name: ACCEPTANCE_GATES.SOURCE_ANALYSIS,
|
|
29
|
+
passed: Boolean(metrics.sourceAnalysis?.passed !== false),
|
|
30
|
+
critical: true,
|
|
31
|
+
},
|
|
32
|
+
astTransformation: {
|
|
33
|
+
name: ACCEPTANCE_GATES.AST_TRANSFORMATION,
|
|
34
|
+
passed: Boolean(metrics.astTransformation?.passed !== false),
|
|
35
|
+
critical: true,
|
|
36
|
+
},
|
|
37
|
+
typescriptValidation: {
|
|
38
|
+
name: ACCEPTANCE_GATES.TYPESCRIPT_VALIDATION,
|
|
39
|
+
passed: Boolean(metrics.typescriptValidation?.passed !== false),
|
|
40
|
+
critical: true,
|
|
41
|
+
},
|
|
42
|
+
buildSuccess: {
|
|
43
|
+
name: ACCEPTANCE_GATES.BUILD_SUCCESS,
|
|
44
|
+
passed: Boolean(metrics.buildSuccess?.passed !== false),
|
|
45
|
+
critical: true,
|
|
46
|
+
},
|
|
47
|
+
fivoraContract: {
|
|
48
|
+
name: ACCEPTANCE_GATES.FIVORA_CONTRACT,
|
|
49
|
+
passed: Boolean(
|
|
50
|
+
metrics.fivoraAudit?.passed === true ||
|
|
51
|
+
metrics.fivoraContract?.passed === true ||
|
|
52
|
+
(metrics.fivoraAudit === undefined && metrics.fivoraContract === undefined)
|
|
53
|
+
),
|
|
54
|
+
critical: true,
|
|
55
|
+
},
|
|
56
|
+
manifestSiteData: {
|
|
57
|
+
name: ACCEPTANCE_GATES.MANIFEST_SITE_DATA,
|
|
58
|
+
passed: Boolean(metrics.manifestValid !== false),
|
|
59
|
+
critical: true,
|
|
60
|
+
},
|
|
61
|
+
runtimeDataBinding: {
|
|
62
|
+
name: ACCEPTANCE_GATES.RUNTIME_DATA_BINDING,
|
|
63
|
+
passed: Boolean((metrics.runtimeEditabilityScore ?? 100) >= 98),
|
|
64
|
+
critical: true,
|
|
65
|
+
},
|
|
66
|
+
runtimeCollectionOps: {
|
|
67
|
+
name: ACCEPTANCE_GATES.RUNTIME_COLLECTION_OPS,
|
|
68
|
+
passed: Boolean(metrics.collectionOpsPassed !== false),
|
|
69
|
+
critical: true,
|
|
70
|
+
},
|
|
71
|
+
runtimeImageEditing: {
|
|
72
|
+
name: ACCEPTANCE_GATES.RUNTIME_IMAGE_EDITING,
|
|
73
|
+
passed: Boolean(metrics.imageEditingPassed !== false),
|
|
74
|
+
critical: false,
|
|
75
|
+
},
|
|
76
|
+
routeCoverage: {
|
|
77
|
+
name: ACCEPTANCE_GATES.ROUTE_COVERAGE,
|
|
78
|
+
passed: Boolean(metrics.routeCoveragePassed !== false),
|
|
79
|
+
critical: false,
|
|
80
|
+
},
|
|
81
|
+
designPreservation: {
|
|
82
|
+
name: ACCEPTANCE_GATES.DESIGN_PRESERVATION,
|
|
83
|
+
passed: Boolean((metrics.designPreservationScore ?? 100) >= 98),
|
|
84
|
+
critical: false,
|
|
85
|
+
},
|
|
86
|
+
controlOnlyProtection: {
|
|
87
|
+
name: ACCEPTANCE_GATES.CONTROL_ONLY_PROTECTION,
|
|
88
|
+
passed: Boolean(metrics.controlOnlyValid !== false),
|
|
89
|
+
critical: true,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const criticalGates = Object.values(gates).filter((g) => g.critical);
|
|
94
|
+
const allCriticalPassed = criticalGates.every((g) => g.passed);
|
|
95
|
+
const editabilityCoverage = metrics.editabilityCoverage ?? 100;
|
|
96
|
+
const designPreservation = metrics.designPreservationScore ?? 100;
|
|
97
|
+
|
|
98
|
+
// Conversion only passes when all critical gates pass and editability coverage >= 98%
|
|
99
|
+
const conversionPassed = allCriticalPassed && editabilityCoverage >= 98;
|
|
100
|
+
const status = conversionPassed
|
|
101
|
+
? 'CONVERSION_PASSED'
|
|
102
|
+
: allCriticalPassed
|
|
103
|
+
? 'CONVERSION_INCOMPLETE'
|
|
104
|
+
: 'CONVERSION_FAILED';
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
passed: conversionPassed,
|
|
108
|
+
status,
|
|
109
|
+
gates,
|
|
110
|
+
allCriticalPassed,
|
|
111
|
+
editabilityCoverage,
|
|
112
|
+
designPreservation,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
ACCEPTANCE_GATES,
|
|
118
|
+
evaluateAcceptanceGates,
|
|
119
|
+
};
|
|
@@ -4,37 +4,82 @@ const recast = require('recast');
|
|
|
4
4
|
const b = recast.types.builders;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Deneb ARC v3 — Canonical Path Engine
|
|
8
|
+
* Single Source of Truth for runtime siteData accessors, visual DOM markers,
|
|
9
|
+
* schema generation, and diagnostics identity.
|
|
9
10
|
*/
|
|
10
11
|
class CanonicalFieldRef {
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} canonicalPath - e.g. "home.hero.title" or "home.categories[0].name"
|
|
14
|
+
* @param {object} [options]
|
|
15
|
+
* @param {object} [options.provenance] - Origin tracking (file, loc, component, kind)
|
|
16
|
+
* @param {boolean} [options.isCollection] - Whether path is an array collection
|
|
17
|
+
*/
|
|
18
|
+
constructor(canonicalPath, options = {}) {
|
|
12
19
|
if (!canonicalPath || typeof canonicalPath !== 'string') {
|
|
13
20
|
throw new Error(`Invalid canonical path: ${canonicalPath}`);
|
|
14
21
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
const cleanPath = canonicalPath.trim().replace(/^\.+|\.+$/g, '');
|
|
23
|
+
if (!cleanPath) {
|
|
24
|
+
throw new Error(`Empty canonical path after normalization: ${canonicalPath}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.path = cleanPath;
|
|
28
|
+
this.canonicalPath = cleanPath;
|
|
29
|
+
this.provenance = options.provenance || {};
|
|
30
|
+
|
|
31
|
+
// Segment split while preserving bracket indices
|
|
32
|
+
this.parts = cleanPath.split('.').filter(Boolean);
|
|
33
|
+
const firstPart = this.parts[0] || 'home';
|
|
34
|
+
this.scope = firstPart.replace(/\[.*$/, '');
|
|
35
|
+
this.section = this.parts.length > 2 ? this.parts[1].replace(/\[.*$/, '') : null;
|
|
36
|
+
this.fieldName = (this.parts[this.parts.length - 1] || '').replace(/\[.*$/, '');
|
|
37
|
+
|
|
38
|
+
this.previewPath = cleanPath;
|
|
39
|
+
this.previewMarker = `data-preview-field-path="${cleanPath}"`;
|
|
40
|
+
this.runtimePath = `siteData.content.${cleanPath}`;
|
|
41
|
+
this.manifestSchemaPath = cleanPath;
|
|
42
|
+
this.diagnosticsId = `field:${cleanPath}`;
|
|
43
|
+
this.isCollection = Boolean(options.isCollection || /\[\d+\]|\[\$\{[^}]+\}\]/.test(cleanPath));
|
|
44
|
+
|
|
45
|
+
// Construct readable runtime accessor string
|
|
46
|
+
const accessorSegments = ['siteData', 'content', ...this.parts];
|
|
47
|
+
this.runtimeAccessor = accessorSegments
|
|
48
|
+
.map((seg, i) => {
|
|
49
|
+
if (i === 0) return seg;
|
|
50
|
+
if (/^([a-zA-Z0-9_-]+)\[(\d+|\$\{[^}]+\})\]$/.test(seg)) {
|
|
51
|
+
const match = seg.match(/^([a-zA-Z0-9_-]+)\[(\d+|\$\{[^}]+\})\]$/);
|
|
52
|
+
return `?.${match[1]}?.[${match[2]}]`;
|
|
53
|
+
}
|
|
54
|
+
return `?.${seg}`;
|
|
55
|
+
})
|
|
56
|
+
.join('');
|
|
20
57
|
}
|
|
21
58
|
|
|
22
59
|
/**
|
|
23
|
-
* Builds
|
|
24
|
-
* siteData?.content?.home?.
|
|
60
|
+
* Builds optional chaining AST expression:
|
|
61
|
+
* siteData?.content?.home?.hero?.title
|
|
25
62
|
*/
|
|
26
63
|
toGetterAst() {
|
|
27
64
|
let expr = b.identifier('siteData');
|
|
28
65
|
const allParts = ['content', ...this.parts];
|
|
29
66
|
for (const part of allParts) {
|
|
30
|
-
|
|
67
|
+
const arrayMatch = part.match(/^([a-zA-Z0-9_-]+)\[(\d+)\]$/);
|
|
68
|
+
if (arrayMatch) {
|
|
69
|
+
const propName = arrayMatch[1];
|
|
70
|
+
const idx = parseInt(arrayMatch[2], 10);
|
|
71
|
+
expr = b.optionalMemberExpression(expr, b.identifier(propName), false, true);
|
|
72
|
+
expr = b.optionalMemberExpression(expr, b.numericLiteral(idx), true, true);
|
|
73
|
+
} else {
|
|
74
|
+
expr = b.optionalMemberExpression(expr, b.identifier(part), false, true);
|
|
75
|
+
}
|
|
31
76
|
}
|
|
32
77
|
return expr;
|
|
33
78
|
}
|
|
34
79
|
|
|
35
80
|
/**
|
|
36
|
-
* Builds
|
|
37
|
-
* siteData?.content?.home?.
|
|
81
|
+
* Builds fallback expression:
|
|
82
|
+
* siteData?.content?.home?.hero?.title ?? fallback
|
|
38
83
|
*/
|
|
39
84
|
toBindingAst(fallbackValue = '') {
|
|
40
85
|
const getter = this.toGetterAst();
|
|
@@ -50,8 +95,8 @@ class CanonicalFieldRef {
|
|
|
50
95
|
}
|
|
51
96
|
|
|
52
97
|
/**
|
|
53
|
-
* Builds
|
|
54
|
-
* data-preview-field-path="home.
|
|
98
|
+
* Builds JSX attribute AST:
|
|
99
|
+
* data-preview-field-path="home.hero.title"
|
|
55
100
|
*/
|
|
56
101
|
toPreviewAttrAst() {
|
|
57
102
|
return b.jsxAttribute(
|
|
@@ -61,9 +106,71 @@ class CanonicalFieldRef {
|
|
|
61
106
|
}
|
|
62
107
|
|
|
63
108
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
109
|
+
* Builds JSX attribute AST for style binding targets:
|
|
110
|
+
* data-preview-style-target="home.hero.title"
|
|
111
|
+
*/
|
|
112
|
+
toPreviewStyleTargetAst() {
|
|
113
|
+
return b.jsxAttribute(
|
|
114
|
+
b.jsxIdentifier('data-preview-style-target'),
|
|
115
|
+
b.stringLiteral(this.path)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generates assignment code for runtime or testing mutations:
|
|
121
|
+
* siteData.content.home.hero.title = value;
|
|
122
|
+
*/
|
|
123
|
+
toSetterCode(valueExpr = 'value') {
|
|
124
|
+
return `siteData.content.${this.path} = ${valueExpr};`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Derives a child item reference for collections:
|
|
129
|
+
* e.g. "home.categories" -> "home.categories[0]"
|
|
130
|
+
*/
|
|
131
|
+
toCollectionItem(index) {
|
|
132
|
+
const itemPath = `${this.path}[${index}]`;
|
|
133
|
+
return new CanonicalFieldRef(itemPath, {
|
|
134
|
+
provenance: { ...this.provenance, parentPath: this.path, index },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Derives a nested collection child reference:
|
|
140
|
+
* e.g. "home.categories[0]" -> "home.categories[0].products[2]"
|
|
141
|
+
*/
|
|
142
|
+
toCollectionChild(childProp, childIndexExpr) {
|
|
143
|
+
const childPath = childIndexExpr !== undefined
|
|
144
|
+
? `${this.path}.${childProp}[${childIndexExpr}]`
|
|
145
|
+
: `${this.path}.${childProp}`;
|
|
146
|
+
return new CanonicalFieldRef(childPath, {
|
|
147
|
+
provenance: { ...this.provenance, parentPath: this.path, childProp },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Validates canonical path syntax and structure
|
|
153
|
+
*/
|
|
154
|
+
validate() {
|
|
155
|
+
if (!isValidCanonicalPath(this.path)) {
|
|
156
|
+
return {
|
|
157
|
+
valid: false,
|
|
158
|
+
error: `Syntax violation in canonical path "${this.path}"`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (!this.scope) {
|
|
162
|
+
return {
|
|
163
|
+
valid: false,
|
|
164
|
+
error: `Missing root scope in canonical path "${this.path}"`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return { valid: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Static helper for nested collection template literals:
|
|
172
|
+
* e.g. parentPath="home.categories[0]", childProp="products", childIndexExpr="index"
|
|
173
|
+
* -> "home.categories[0].products[${index}]"
|
|
67
174
|
*/
|
|
68
175
|
static buildNestedItemPath(parentItemPath, childProp, childIndexExpr = 'index') {
|
|
69
176
|
return `${parentItemPath}.${childProp}[\${${childIndexExpr}}]`;
|
|
@@ -72,23 +179,35 @@ class CanonicalFieldRef {
|
|
|
72
179
|
|
|
73
180
|
function isValidCanonicalPath(canonicalPath) {
|
|
74
181
|
if (typeof canonicalPath !== 'string' || !canonicalPath.trim()) return false;
|
|
75
|
-
return /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+|\[\d+\]|\[\$\{[^}]+\}\])*$/.test(canonicalPath);
|
|
182
|
+
return /^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+|\[\d+\]|\[\$\{[^}]+\}\])*$/.test(canonicalPath.trim());
|
|
76
183
|
}
|
|
77
184
|
|
|
78
185
|
function validatePathAlignment(getterCode, previewAttrCode) {
|
|
79
186
|
const normalizedGetter = String(getterCode || '').replace(/\?\./g, '.');
|
|
80
|
-
const getterMatch = normalizedGetter.match(/siteData\.content\.([a-zA-Z0-9_.]+)/);
|
|
81
|
-
const attrMatch = String(previewAttrCode || '').match(/data-preview-field-path=["'`{]([a-zA-Z0-9_.]+)/);
|
|
187
|
+
const getterMatch = normalizedGetter.match(/siteData\.content\.([a-zA-Z0-9_.[\]]+)/);
|
|
188
|
+
const attrMatch = String(previewAttrCode || '').match(/data-preview-field-path=["'`{]([a-zA-Z0-9_.[\]]+)/);
|
|
82
189
|
if (!getterMatch || !attrMatch) return false;
|
|
83
190
|
return getterMatch[1] === attrMatch[1];
|
|
84
191
|
}
|
|
85
192
|
|
|
86
|
-
|
|
87
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Primary factory function for CanonicalFieldRef
|
|
195
|
+
* Single source of truth across compiler, transformer, and runtime validator.
|
|
196
|
+
*/
|
|
197
|
+
function canonicalField(rawPath, options) {
|
|
198
|
+
return new CanonicalFieldRef(rawPath, options);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Backward-compatible alias
|
|
203
|
+
*/
|
|
204
|
+
function fieldRef(canonicalPath, options) {
|
|
205
|
+
return canonicalField(canonicalPath, options);
|
|
88
206
|
}
|
|
89
207
|
|
|
90
208
|
module.exports = {
|
|
91
209
|
CanonicalFieldRef,
|
|
210
|
+
canonicalField,
|
|
92
211
|
fieldRef,
|
|
93
212
|
isValidCanonicalPath,
|
|
94
213
|
validatePathAlignment,
|