@pantheon-systems/p1-content-validator 1.0.0 → 2.0.0
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/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/structure-validator.d.ts +26 -0
- package/dist/structure-validator.d.ts.map +1 -0
- package/dist/structure-validator.js +118 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { validateOps } from './validator.js';
|
|
2
2
|
export { fetchRegistry, listRegistryVersions, snapshotToComponentSchema } from './registry.js';
|
|
3
|
-
export
|
|
3
|
+
export { validateDocumentStructure } from './structure-validator.js';
|
|
4
|
+
export type { EditOperation, ComponentSchema, ComponentField, FieldOption, ValidationError, ValidateInput, FetchRegistryOpts, TemplateComponent, TemplateSnapshot, StructuralConformanceError, ValidateStructureInput, } from './types.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/F,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,EACd,WAAW,EACX,eAAe,EACf,aAAa,EACb,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,eAAe,EACf,cAAc,EACd,WAAW,EACX,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { StructuralConformanceError, ValidateStructureInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates that a document snapshot conforms to a template's structural skeleton.
|
|
4
|
+
*
|
|
5
|
+
* A template component is pinned when `root.props._pinMap[props.id]` is `true`;
|
|
6
|
+
* pinned types are checked in template `content` order.
|
|
7
|
+
*
|
|
8
|
+
* Conformance rules:
|
|
9
|
+
* 1. All pinned components must be present in the document
|
|
10
|
+
* 2. Pinned components must appear in the same relative order as in the template
|
|
11
|
+
* 3. Non-pinned components are allowed and do not affect conformance
|
|
12
|
+
*
|
|
13
|
+
* This implements partial conformance: documents may have additional components
|
|
14
|
+
* beyond the pinned skeleton without failing validation.
|
|
15
|
+
*
|
|
16
|
+
* DEFENSIVE DESIGN: This function is designed to never crash, even with malformed
|
|
17
|
+
* inputs. All property accesses use optional chaining, and all array operations
|
|
18
|
+
* verify types before use.
|
|
19
|
+
*
|
|
20
|
+
* @param input - Document snapshot and template snapshot to validate against
|
|
21
|
+
* @returns Object with array of structural conformance errors (empty if valid)
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateDocumentStructure(input: ValidateStructureInput): {
|
|
24
|
+
errors: StructuralConformanceError[];
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=structure-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structure-validator.d.ts","sourceRoot":"","sources":["../src/structure-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,YAAY,CAAC;AASpB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,sBAAsB,GAC5B;IAAE,MAAM,EAAE,0BAA0B,EAAE,CAAA;CAAE,CAoG1C"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Returns the value when it is a plain object, otherwise undefined. */
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
4
|
+
? value
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Validates that a document snapshot conforms to a template's structural skeleton.
|
|
9
|
+
*
|
|
10
|
+
* A template component is pinned when `root.props._pinMap[props.id]` is `true`;
|
|
11
|
+
* pinned types are checked in template `content` order.
|
|
12
|
+
*
|
|
13
|
+
* Conformance rules:
|
|
14
|
+
* 1. All pinned components must be present in the document
|
|
15
|
+
* 2. Pinned components must appear in the same relative order as in the template
|
|
16
|
+
* 3. Non-pinned components are allowed and do not affect conformance
|
|
17
|
+
*
|
|
18
|
+
* This implements partial conformance: documents may have additional components
|
|
19
|
+
* beyond the pinned skeleton without failing validation.
|
|
20
|
+
*
|
|
21
|
+
* DEFENSIVE DESIGN: This function is designed to never crash, even with malformed
|
|
22
|
+
* inputs. All property accesses use optional chaining, and all array operations
|
|
23
|
+
* verify types before use.
|
|
24
|
+
*
|
|
25
|
+
* @param input - Document snapshot and template snapshot to validate against
|
|
26
|
+
* @returns Object with array of structural conformance errors (empty if valid)
|
|
27
|
+
*/
|
|
28
|
+
export function validateDocumentStructure(input) {
|
|
29
|
+
const { documentSnapshot, templateSnapshot } = input;
|
|
30
|
+
const errors = [];
|
|
31
|
+
// Puck snapshots store the component array at top-level `content`, but some
|
|
32
|
+
// wrappers nest it under `root.props.content`. Check both, fall back to empty.
|
|
33
|
+
const topLevelContent = documentSnapshot?.content;
|
|
34
|
+
const contentRaw = Array.isArray(topLevelContent)
|
|
35
|
+
? topLevelContent
|
|
36
|
+
: asRecord(asRecord(documentSnapshot?.root)?.props)?.content;
|
|
37
|
+
const content = Array.isArray(contentRaw)
|
|
38
|
+
? contentRaw
|
|
39
|
+
: [];
|
|
40
|
+
const templateContent = Array.isArray(templateSnapshot?.content)
|
|
41
|
+
? templateSnapshot.content
|
|
42
|
+
: [];
|
|
43
|
+
// A component is pinned only when root.props._pinMap[props.id] is true.
|
|
44
|
+
const pinMap = asRecord(asRecord(asRecord(templateSnapshot?.root)?.props)?._pinMap) ?? {};
|
|
45
|
+
// Pinned component types in template content order.
|
|
46
|
+
// A component without a string type or string id is never pinned.
|
|
47
|
+
const pinnedTypes = [];
|
|
48
|
+
for (const templateComponent of templateContent) {
|
|
49
|
+
if (!templateComponent || typeof templateComponent.type !== 'string') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const id = asRecord(templateComponent.props)?.id;
|
|
53
|
+
if (typeof id === 'string' && pinMap[id] === true) {
|
|
54
|
+
pinnedTypes.push(templateComponent.type);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// If template has no pinned components, document always conforms
|
|
58
|
+
if (pinnedTypes.length === 0) {
|
|
59
|
+
return { errors };
|
|
60
|
+
}
|
|
61
|
+
// Build a map of pinned component types to their indices in the document
|
|
62
|
+
// Defensively handle components without type field or with non-string types
|
|
63
|
+
const pinnedIndices = new Map();
|
|
64
|
+
content.forEach((component, index) => {
|
|
65
|
+
// Skip components without a valid type field
|
|
66
|
+
if (!component || typeof component.type !== 'string') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (pinnedTypes.includes(component.type)) {
|
|
70
|
+
const indices = pinnedIndices.get(component.type) || [];
|
|
71
|
+
indices.push(index);
|
|
72
|
+
pinnedIndices.set(component.type, indices);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Track the last index we successfully matched to ensure ordering
|
|
76
|
+
let lastFoundIndex = -1;
|
|
77
|
+
// Check each pinned component in template order
|
|
78
|
+
for (let i = 0; i < pinnedTypes.length; i++) {
|
|
79
|
+
const expectedType = pinnedTypes[i];
|
|
80
|
+
const indices = pinnedIndices.get(expectedType) || [];
|
|
81
|
+
// Find the first occurrence of this component type after lastFoundIndex
|
|
82
|
+
const foundIndex = indices.find((idx) => idx > lastFoundIndex);
|
|
83
|
+
if (foundIndex === undefined) {
|
|
84
|
+
// No unconsumed occurrence appears after the last match. If the document
|
|
85
|
+
// holds fewer instances of this type than the template pins up to here,
|
|
86
|
+
// a required instance is absent; otherwise the instances that exist sit
|
|
87
|
+
// before where this one must appear.
|
|
88
|
+
let requiredSoFar = 0;
|
|
89
|
+
for (let j = 0; j <= i; j++) {
|
|
90
|
+
if (pinnedTypes[j] === expectedType)
|
|
91
|
+
requiredSoFar++;
|
|
92
|
+
}
|
|
93
|
+
if (indices.length < requiredSoFar) {
|
|
94
|
+
errors.push({
|
|
95
|
+
code: 'missing_pinned_component',
|
|
96
|
+
componentType: expectedType,
|
|
97
|
+
message: `Required component "${expectedType}" is missing from the document.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// Component exists but is out of order (appears before lastFoundIndex)
|
|
102
|
+
errors.push({
|
|
103
|
+
code: 'pinned_component_out_of_order',
|
|
104
|
+
componentType: expectedType,
|
|
105
|
+
expectedIndex: i,
|
|
106
|
+
actualIndex: indices[0],
|
|
107
|
+
message: `Pinned component "${expectedType}" appears out of order. ` +
|
|
108
|
+
`Expected after index ${lastFoundIndex} but found at index ${indices[0]}.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Component found in correct relative order
|
|
114
|
+
lastFoundIndex = foundIndex;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { errors };
|
|
118
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -44,4 +44,36 @@ export interface ValidateInput {
|
|
|
44
44
|
warnOnZonesUsage?: boolean;
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
export interface TemplateComponent {
|
|
48
|
+
type: string;
|
|
49
|
+
/** Component props are the template's default props. `id` keys into `_pinMap`. */
|
|
50
|
+
props: {
|
|
51
|
+
id?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** A template's version snapshot: Puck data, identical in shape to a page. */
|
|
56
|
+
export interface TemplateSnapshot {
|
|
57
|
+
content: TemplateComponent[];
|
|
58
|
+
root: {
|
|
59
|
+
props: {
|
|
60
|
+
_template?: Record<string, unknown>;
|
|
61
|
+
/** Component id to pinned flag. A component is pinned only when its entry is `true`. */
|
|
62
|
+
_pinMap?: Record<string, boolean>;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
zones?: Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
export interface StructuralConformanceError {
|
|
69
|
+
code: 'missing_pinned_component' | 'pinned_component_out_of_order' | 'unexpected_component_at_pinned_slot';
|
|
70
|
+
message: string;
|
|
71
|
+
componentType: string;
|
|
72
|
+
expectedIndex?: number;
|
|
73
|
+
actualIndex?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface ValidateStructureInput {
|
|
76
|
+
documentSnapshot: Record<string, unknown>;
|
|
77
|
+
templateSnapshot: TemplateSnapshot;
|
|
78
|
+
}
|
|
47
79
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EACA,wBAAwB,GACxB,kBAAkB,GAClB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,wBAAwB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,CAAC;CACH"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon-systems/p1-content-validator",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Validates Puck component edit operations against the CSS component registry",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=20.0.0"
|
|
30
30
|
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/",
|
|
34
|
+
"provenance": false
|
|
35
|
+
},
|
|
31
36
|
"license": "MIT",
|
|
32
37
|
"scripts": {
|
|
33
38
|
"build": "tsc",
|