@pantheon-systems/p1-content-validator 2.1.2 → 2.2.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/README.md +2 -2
- package/dist/authority-enforcement.d.ts.map +1 -1
- package/dist/authority-enforcement.js +10 -25
- package/dist/guards.js +0 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/localization.d.ts +5 -0
- package/dist/localization.d.ts.map +1 -1
- package/dist/localization.js +1 -40
- package/dist/registry.d.ts +5 -5
- package/dist/registry.js +9 -72
- package/dist/structure-validator.js +0 -33
- package/dist/validator.js +2 -73
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pantheon-systems/p1-content-validator
|
|
2
2
|
|
|
3
|
-
Validates Puck component edit operations against
|
|
3
|
+
Validates Puck component edit operations against Pantheon's P1 component
|
|
4
4
|
registry, so writes that don't match a component's registered prop shape are rejected before
|
|
5
5
|
they reach a document.
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ const result = validateOps(ops, registry);
|
|
|
25
25
|
|
|
26
26
|
Operations are checked against the registry entry for each component — unknown components,
|
|
27
27
|
unknown props, and type mismatches are reported rather than silently written. This is the same
|
|
28
|
-
validation the
|
|
28
|
+
validation the backend applies to AI-assisted and API-driven edits.
|
|
29
29
|
|
|
30
30
|
Also exported:
|
|
31
31
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authority-enforcement.d.ts","sourceRoot":"","sources":["../src/authority-enforcement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EACnB,iCAAiC,EAClC,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"authority-enforcement.d.ts","sourceRoot":"","sources":["../src/authority-enforcement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EACnB,iCAAiC,EAClC,MAAM,YAAY,CAAC;AAgDpB,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,iCAAiC,GACvC;IAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,CA4ExC"}
|
|
@@ -1,35 +1,22 @@
|
|
|
1
|
-
import { isAuthority, resolveSlotAuthority } from './localization.js';
|
|
1
|
+
import { isAuthority, resolveSlotAuthority, ROOT_SLOT_ID } from './localization.js';
|
|
2
2
|
import { isPlainObject, resolvePropPath } from './guards.js';
|
|
3
|
-
/**
|
|
4
|
-
* Authority enforcement for writes against a translation document.
|
|
5
|
-
*
|
|
6
|
-
* A translation owns its `locale`-authority props and inherits its
|
|
7
|
-
* `canonical`-authority props from the canonical it derives from. Writing a
|
|
8
|
-
* `canonical`-authority prop outside the sync/reconcile workflow is reported as a
|
|
9
|
-
* diagnostic at the caller's severity, never rejected here.
|
|
10
|
-
*
|
|
11
|
-
* This module is pure: the caller resolves the template snapshot and the
|
|
12
|
-
* localization edge's per-prop overrides and passes them in.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
* Resolves the component a prop-path op targets and returns its slot id, or
|
|
16
|
-
* `undefined` when the path does not target a prop on a resolvable component.
|
|
17
|
-
* Authority is keyed by slot id, so a component without one is not judged.
|
|
18
|
-
*/
|
|
19
3
|
function resolveSlot(path, snapshot) {
|
|
4
|
+
const parts = path.split('.');
|
|
5
|
+
if (parts[0] === 'root' && parts.lastIndexOf('props') === 1) {
|
|
6
|
+
return { slotId: ROOT_SLOT_ID, propsIdx: 1, parts };
|
|
7
|
+
}
|
|
20
8
|
const resolved = resolvePropPath(path, snapshot);
|
|
21
9
|
if (resolved === undefined || typeof resolved.component.props.id !== 'string') {
|
|
22
10
|
return undefined;
|
|
23
11
|
}
|
|
24
12
|
return { slotId: resolved.component.props.id, propsIdx: resolved.propsIdx, parts: resolved.parts };
|
|
25
13
|
}
|
|
14
|
+
function isContentProp(propName) {
|
|
15
|
+
return propName !== 'id' && !propName.startsWith('_');
|
|
16
|
+
}
|
|
26
17
|
export function validateTranslationAuthority(input) {
|
|
27
18
|
const { operations, currentSnapshot, templateSnapshot, authorityOverrides = {}, slotAuthority = {}, severity = 'warning', } = input;
|
|
28
19
|
const diagnostics = [];
|
|
29
|
-
// Slot ids and prop names are caller-chosen keys arriving as parsed JSON, so both
|
|
30
|
-
// maps are read through a Map: a missing key stays missing instead of resolving to
|
|
31
|
-
// an Object.prototype member. A stored value that is not an authority is dropped,
|
|
32
|
-
// leaving its prop on `canonical`.
|
|
33
20
|
const overrides = new Map(Object.entries(authorityOverrides).map(([slotId, props]) => [
|
|
34
21
|
slotId,
|
|
35
22
|
new Map(Object.entries(props).filter(([, value]) => isAuthority(value))),
|
|
@@ -60,12 +47,11 @@ export function validateTranslationAuthority(input) {
|
|
|
60
47
|
if (resolved === undefined)
|
|
61
48
|
return;
|
|
62
49
|
const { slotId, propsIdx, parts } = resolved;
|
|
63
|
-
// Case A: the path ends at `.props` — the content is the whole props object.
|
|
64
50
|
if (propsIdx === parts.length - 1) {
|
|
65
51
|
if (!isPlainObject(op.content))
|
|
66
52
|
return;
|
|
67
53
|
for (const propName of Object.keys(op.content)) {
|
|
68
|
-
if (propName
|
|
54
|
+
if (!isContentProp(propName))
|
|
69
55
|
continue;
|
|
70
56
|
if (effectiveAuthority(slotId, propName) === 'canonical') {
|
|
71
57
|
flag(opIndex, `${op.path}.${propName}`, slotId, propName);
|
|
@@ -73,9 +59,8 @@ export function validateTranslationAuthority(input) {
|
|
|
73
59
|
}
|
|
74
60
|
return;
|
|
75
61
|
}
|
|
76
|
-
// Case B: the path targets a single prop — `.props.<name>`.
|
|
77
62
|
const propName = parts[propsIdx + 1];
|
|
78
|
-
if (propName
|
|
63
|
+
if (!isContentProp(propName))
|
|
79
64
|
return;
|
|
80
65
|
if (effectiveAuthority(slotId, propName) === 'canonical') {
|
|
81
66
|
flag(opIndex, op.path, slotId, propName);
|
package/dist/guards.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shape guards and path reads shared by the validators. Every entry point takes
|
|
3
|
-
* `unknown` — snapshots, templates, and operations all arrive as parsed JSON.
|
|
4
|
-
*/
|
|
5
|
-
/** Whether a value is a non-null, non-array object. */
|
|
6
1
|
export function isPlainObject(value) {
|
|
7
2
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
3
|
}
|
|
9
|
-
/** Whether a value has the shape of a component: a string `type` and a props object. */
|
|
10
4
|
export function isComponentShape(value) {
|
|
11
5
|
return isPlainObject(value) && typeof value.type === 'string' && isPlainObject(value.props);
|
|
12
6
|
}
|
|
13
|
-
/**
|
|
14
|
-
* Reads the value at a dot-notation path (`content.0.props.title`), treating a
|
|
15
|
-
* numeric segment as an array index. An empty path reads the root. Resolves to
|
|
16
|
-
* undefined when a segment is absent or the path runs into a non-object.
|
|
17
|
-
*/
|
|
18
7
|
export function getAtPath(obj, path) {
|
|
19
8
|
if (path === '')
|
|
20
9
|
return obj;
|
|
@@ -30,20 +19,9 @@ export function getAtPath(obj, path) {
|
|
|
30
19
|
return cur[key];
|
|
31
20
|
}, obj);
|
|
32
21
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Resolves the component whose prop a path writes to.
|
|
35
|
-
*
|
|
36
|
-
* Components nest through their slot props, so a path can carry several `props`
|
|
37
|
-
* segments (`content.0.props.items.1.props.title`). The last one belongs to the
|
|
38
|
-
* component being written; the earlier ones are its ancestors.
|
|
39
|
-
*
|
|
40
|
-
* Resolves to undefined when the path names no component before its props, or when
|
|
41
|
-
* the snapshot holds something other than a component there.
|
|
42
|
-
*/
|
|
43
22
|
export function resolvePropPath(path, snapshot) {
|
|
44
23
|
const parts = path.split('.');
|
|
45
24
|
const propsIdx = parts.lastIndexOf('props');
|
|
46
|
-
// A prop write has at least one segment before 'props' (the component path).
|
|
47
25
|
if (propsIdx <= 0) {
|
|
48
26
|
return undefined;
|
|
49
27
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { validateOps } from './validator.js';
|
|
|
2
2
|
export { fetchRegistry, listRegistryVersions, snapshotToComponentSchema, registryComponentKey, componentNameFromPath, } from './registry.js';
|
|
3
3
|
export { validateDocumentStructure } from './structure-validator.js';
|
|
4
4
|
export { validateTranslationAuthority } from './authority-enforcement.js';
|
|
5
|
-
export { resolveTranslatable, resolveSlotAuthority, resolveSlotAuthorityMap, isAuthority, AUTHORITIES, DEFAULT_AUTHORITY, } from './localization.js';
|
|
5
|
+
export { resolveTranslatable, resolveSlotAuthority, resolveSlotAuthorityMap, isAuthority, AUTHORITIES, DEFAULT_AUTHORITY, ROOT_SLOT_ID, } from './localization.js';
|
|
6
6
|
export type { EditOperation, Authority, ComponentSchema, ComponentField, FieldOption, ValidationError, ValidateInput, FetchRegistryOpts, TemplateComponent, TemplateSnapshot, StructuralConformanceError, ValidateStructureInput, AuthoritySeverity, AuthorityOverrideMap, AuthorityDiagnostic, ValidateTranslationAuthorityInput, } from './types.js';
|
|
7
7
|
//# 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,EACL,aAAa,EACb,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,WAAW,EACX,WAAW,EACX,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,EACL,aAAa,EACb,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,aAAa,EACb,SAAS,EACT,eAAe,EACf,cAAc,EACd,WAAW,EACX,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,GAClC,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { validateOps } from './validator.js';
|
|
|
2
2
|
export { fetchRegistry, listRegistryVersions, snapshotToComponentSchema, registryComponentKey, componentNameFromPath, } from './registry.js';
|
|
3
3
|
export { validateDocumentStructure } from './structure-validator.js';
|
|
4
4
|
export { validateTranslationAuthority } from './authority-enforcement.js';
|
|
5
|
-
export { resolveTranslatable, resolveSlotAuthority, resolveSlotAuthorityMap, isAuthority, AUTHORITIES, DEFAULT_AUTHORITY, } from './localization.js';
|
|
5
|
+
export { resolveTranslatable, resolveSlotAuthority, resolveSlotAuthorityMap, isAuthority, AUTHORITIES, DEFAULT_AUTHORITY, ROOT_SLOT_ID, } from './localization.js';
|
package/dist/localization.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ import type { Authority } from './types.js';
|
|
|
15
15
|
export declare const AUTHORITIES: readonly ["canonical", "locale"];
|
|
16
16
|
/** The authority a slot falls back to when the template declares none. */
|
|
17
17
|
export declare const DEFAULT_AUTHORITY: Authority;
|
|
18
|
+
/**
|
|
19
|
+
* The slot id root props are keyed by. Authority and translatability are keyed by
|
|
20
|
+
* the slot id of the component owning a prop; root props belong to no component.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ROOT_SLOT_ID = "__root__";
|
|
18
23
|
/** Whether a value is one of the two authorities. */
|
|
19
24
|
export declare function isAuthority(value: unknown): value is Authority;
|
|
20
25
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"localization.d.ts","sourceRoot":"","sources":["../src/localization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;;;;;;;;GAWG;AAEH,gDAAgD;AAChD,eAAO,MAAM,WAAW,kCAAmC,CAAC;AAE5D,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAuB,CAAC;AAExD,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D;AAmBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAGzF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAM5F"}
|
|
1
|
+
{"version":3,"file":"localization.d.ts","sourceRoot":"","sources":["../src/localization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;;;;;;;;GAWG;AAEH,gDAAgD;AAChD,eAAO,MAAM,WAAW,kCAAmC,CAAC;AAE5D,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,EAAE,SAAuB,CAAC;AAExD;;;GAGG;AACH,eAAO,MAAM,YAAY,aAAa,CAAC;AAEvC,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D;AAmBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,gBAAgB,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAGzF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAM5F"}
|
package/dist/localization.js
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
import { isPlainObject } from './guards.js';
|
|
2
|
-
/**
|
|
3
|
-
* Localization resolvers — the single source of truth for two independent
|
|
4
|
-
* properties of the localization model:
|
|
5
|
-
*
|
|
6
|
-
* - `translatable`: whether a prop holds natural-language text a human would
|
|
7
|
-
* translate. A per-(slotId, propName) flag that defaults to true, stored once
|
|
8
|
-
* on the canonical page (it applies to every language variant) and resolved
|
|
9
|
-
* via `resolveTranslatable`.
|
|
10
|
-
* - `authority`: a property of the RELATIONSHIP between a translation and its
|
|
11
|
-
* canonical. The per-slot default is declared on the template's
|
|
12
|
-
* `_localeAuthority` map and resolved via `resolveSlotAuthority`.
|
|
13
|
-
*/
|
|
14
|
-
/** Every authority a slot or prop can carry. */
|
|
15
2
|
export const AUTHORITIES = ['canonical', 'locale'];
|
|
16
|
-
/** The authority a slot falls back to when the template declares none. */
|
|
17
3
|
export const DEFAULT_AUTHORITY = 'canonical';
|
|
18
|
-
|
|
4
|
+
export const ROOT_SLOT_ID = '__root__';
|
|
19
5
|
export function isAuthority(value) {
|
|
20
6
|
return AUTHORITIES.includes(value);
|
|
21
7
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Reads one of the localization config maps off a snapshot's `root.props`. Absent
|
|
24
|
-
* maps and malformed snapshots read as an empty map, so a resolver's fallback is
|
|
25
|
-
* the same whether the map is missing or the snapshot is not content-shaped.
|
|
26
|
-
*/
|
|
27
8
|
function localeConfigMap(snapshot, key) {
|
|
28
9
|
if (!isPlainObject(snapshot)) {
|
|
29
10
|
return {};
|
|
@@ -35,14 +16,6 @@ function localeConfigMap(snapshot, key) {
|
|
|
35
16
|
const map = root.props[key];
|
|
36
17
|
return isPlainObject(map) ? map : {};
|
|
37
18
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Resolves whether a prop is translatable from the canonical page snapshot's
|
|
40
|
-
* `root.props._localeTranslatable` map, keyed by slot id then prop name. The flag
|
|
41
|
-
* applies to every language variant, so it lives on the canonical only. A prop is
|
|
42
|
-
* translatable unless an entry is explicitly stored as `false`; absent entries,
|
|
43
|
-
* absent maps, malformed snapshots, and non-boolean stored values all resolve to
|
|
44
|
-
* true.
|
|
45
|
-
*/
|
|
46
19
|
export function resolveTranslatable(snapshot, slotId, propName) {
|
|
47
20
|
const slot = localeConfigMap(snapshot, '_localeTranslatable')[slotId];
|
|
48
21
|
if (!isPlainObject(slot)) {
|
|
@@ -50,22 +23,10 @@ export function resolveTranslatable(snapshot, slotId, propName) {
|
|
|
50
23
|
}
|
|
51
24
|
return slot[propName] !== false;
|
|
52
25
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Resolves the per-slot authority default from a template snapshot's
|
|
55
|
-
* `root.props._localeAuthority` map, keyed by slot id. Slots with no declared
|
|
56
|
-
* authority, absent maps, malformed snapshots, and unrecognized stored values
|
|
57
|
-
* all resolve to `canonical`.
|
|
58
|
-
*/
|
|
59
26
|
export function resolveSlotAuthority(templateSnapshot, slotId) {
|
|
60
27
|
const value = localeConfigMap(templateSnapshot, '_localeAuthority')[slotId];
|
|
61
28
|
return isAuthority(value) ? value : DEFAULT_AUTHORITY;
|
|
62
29
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Resolves a template's whole `root.props._localeAuthority` map, keyed by slot id.
|
|
65
|
-
* Slots storing an unrecognized value are omitted, so a slot present in the result
|
|
66
|
-
* carries a declared authority and a slot absent from it falls back to
|
|
67
|
-
* `DEFAULT_AUTHORITY`. Absent maps and malformed snapshots resolve to no slots.
|
|
68
|
-
*/
|
|
69
30
|
export function resolveSlotAuthorityMap(templateSnapshot) {
|
|
70
31
|
return Object.fromEntries(Object.entries(localeConfigMap(templateSnapshot, '_localeAuthority')).filter((entry) => isAuthority(entry[1])));
|
|
71
32
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -23,26 +23,26 @@ export declare function componentNameFromPath(path: string): string;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function snapshotToComponentSchema(snapshot: Record<string, unknown>): ComponentSchema | null;
|
|
25
25
|
/**
|
|
26
|
-
* Fetch and cache all component schemas from the
|
|
26
|
+
* Fetch and cache all component schemas from the registry.
|
|
27
27
|
*
|
|
28
28
|
* Makes two types of requests:
|
|
29
29
|
* 1. GET /api/sites/{siteId}/branches/{branchId}/documents?pathPrefix=_registry%2Fcomponents%2F
|
|
30
30
|
* 2. GET .../versions/latest for each component document
|
|
31
31
|
*
|
|
32
|
-
* Results are cached per (
|
|
32
|
+
* Results are cached per (ccrBaseUrl, siteId, branchId) with a 5-minute TTL.
|
|
33
33
|
* If the cache is fresh, no network calls are made.
|
|
34
34
|
*/
|
|
35
|
-
export declare function fetchRegistry(
|
|
35
|
+
export declare function fetchRegistry(ccrBaseUrl: string, siteId: string, branchId: string, opts: FetchRegistryOpts): Promise<Record<string, ComponentSchema>>;
|
|
36
36
|
/**
|
|
37
37
|
* Metadata-only listing: returns one entry per component with its document ID.
|
|
38
38
|
* Useful for cache invalidation checks — compare IDs against a local cache
|
|
39
39
|
* to detect which component schemas have been updated without fetching bodies.
|
|
40
40
|
*
|
|
41
|
-
* Note: the
|
|
41
|
+
* Note: the list endpoint does not currently expose versionId; the document
|
|
42
42
|
* id is returned as a stable identifier. Full version-id tracking requires a
|
|
43
43
|
* backend extension (tracked separately).
|
|
44
44
|
*/
|
|
45
|
-
export declare function listRegistryVersions(
|
|
45
|
+
export declare function listRegistryVersions(ccrBaseUrl: string, siteId: string, branchId: string, opts: FetchRegistryOpts): Promise<{
|
|
46
46
|
name: string;
|
|
47
47
|
versionId: string;
|
|
48
48
|
}[]>;
|
package/dist/registry.js
CHANGED
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Registry key normalization
|
|
3
|
-
//
|
|
4
|
-
// Document paths (including "_registry/components/{Name}") are lowercased
|
|
5
|
-
// server-side on write (see workers/src/services/document-types.ts
|
|
6
|
-
// normalizePath), but the component's real, original-case name is preserved
|
|
7
|
-
// in the descriptor snapshot body's own `name` field. Lookups against the
|
|
8
|
-
// registry must therefore be case-insensitive, while the schema's `name`
|
|
9
|
-
// (what's stored/displayed to callers) stays the true original case.
|
|
10
|
-
//
|
|
11
|
-
// Mirrors the `registryComponentKey` pattern established in p1-chatbot /
|
|
12
|
-
// puck-css-integration#122 for the client-side registry — same name, same
|
|
13
|
-
// approach, so both sides of the registry stay consistent.
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
1
|
export function registryComponentKey(name) {
|
|
16
2
|
return name.toLowerCase();
|
|
17
3
|
}
|
|
18
4
|
const REGISTRY_COMPONENTS_PATH_PREFIX = '_registry/components/';
|
|
19
|
-
/**
|
|
20
|
-
* Derives a component's fallback name from its registry document path
|
|
21
|
-
* (e.g. "_registry/components/leadcapture" -> "leadcapture"). This is a
|
|
22
|
-
* fallback only — paths are lowercased server-side, so the result does not
|
|
23
|
-
* reflect the component's real casing (e.g. "LeadCapture"). The descriptor
|
|
24
|
-
* snapshot's own `name` field (see `snapshotToComponentSchema`) is the
|
|
25
|
-
* source of truth for the real, original-case name.
|
|
26
|
-
*/
|
|
27
5
|
export function componentNameFromPath(path) {
|
|
28
6
|
return path.slice(REGISTRY_COMPONENTS_PATH_PREFIX.length);
|
|
29
7
|
}
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Shared snapshot → ComponentSchema transformation
|
|
32
|
-
// Used by both fetchRegistry (raw fetch path) and McpApiClient.fetchRegistrySchemas
|
|
33
|
-
// (circuit-breaker-wrapped path) to ensure consistent extraction logic.
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
/**
|
|
36
|
-
* Returns null when the descriptor carries no usable `name`.
|
|
37
|
-
*
|
|
38
|
-
* There used to be a fallback to the path-derived name here. A document path
|
|
39
|
-
* cannot carry a component's casing reliably, so that fallback invented one and
|
|
40
|
-
* advertised it as a valid component type — teaching every consumer (and every
|
|
41
|
-
* agent calling list_components) a casing Puck may not resolve. A descriptor
|
|
42
|
-
* with no `name` is corrupt; callers must skip and log it rather than guess.
|
|
43
|
-
*
|
|
44
|
-
* This holds however paths are stored: the descriptor body is the source of
|
|
45
|
-
* truth for casing, so the path is never the place to recover it from.
|
|
46
|
-
*/
|
|
47
8
|
export function snapshotToComponentSchema(snapshot) {
|
|
48
9
|
if (typeof snapshot.name !== 'string' || snapshot.name === '') {
|
|
49
10
|
return null;
|
|
@@ -63,30 +24,17 @@ export function snapshotToComponentSchema(snapshot) {
|
|
|
63
24
|
};
|
|
64
25
|
}
|
|
65
26
|
const cache = new Map();
|
|
66
|
-
const TTL_MS = 5 * 60 * 1000;
|
|
67
|
-
function cacheKey(
|
|
68
|
-
return `${
|
|
27
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
28
|
+
function cacheKey(ccrBaseUrl, siteId, branchId) {
|
|
29
|
+
return `${ccrBaseUrl}:${siteId}:${branchId}`;
|
|
69
30
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
/**
|
|
74
|
-
* Fetch and cache all component schemas from the CSS registry.
|
|
75
|
-
*
|
|
76
|
-
* Makes two types of requests:
|
|
77
|
-
* 1. GET /api/sites/{siteId}/branches/{branchId}/documents?pathPrefix=_registry%2Fcomponents%2F
|
|
78
|
-
* 2. GET .../versions/latest for each component document
|
|
79
|
-
*
|
|
80
|
-
* Results are cached per (cssBaseUrl, siteId, branchId) with a 5-minute TTL.
|
|
81
|
-
* If the cache is fresh, no network calls are made.
|
|
82
|
-
*/
|
|
83
|
-
export async function fetchRegistry(cssBaseUrl, siteId, branchId, opts) {
|
|
84
|
-
const key = cacheKey(cssBaseUrl, siteId, branchId);
|
|
31
|
+
export async function fetchRegistry(ccrBaseUrl, siteId, branchId, opts) {
|
|
32
|
+
const key = cacheKey(ccrBaseUrl, siteId, branchId);
|
|
85
33
|
const cached = cache.get(key);
|
|
86
34
|
if (cached !== undefined && Date.now() - cached.cachedAt < TTL_MS) {
|
|
87
35
|
return cached.schemas;
|
|
88
36
|
}
|
|
89
|
-
const base =
|
|
37
|
+
const base = ccrBaseUrl.replace(/\/$/, '');
|
|
90
38
|
const headers = {
|
|
91
39
|
'Content-Type': 'application/json',
|
|
92
40
|
'X-API-Key': opts.token,
|
|
@@ -99,7 +47,6 @@ export async function fetchRegistry(cssBaseUrl, siteId, branchId, opts) {
|
|
|
99
47
|
}
|
|
100
48
|
const { documents } = (await listRes.json());
|
|
101
49
|
if (documents.length === 0) {
|
|
102
|
-
// Cache the empty result so we don't re-hit listDocuments on every call within the TTL
|
|
103
50
|
cache.set(key, { cachedAt: Date.now(), schemas: {} });
|
|
104
51
|
return {};
|
|
105
52
|
}
|
|
@@ -121,23 +68,13 @@ export async function fetchRegistry(cssBaseUrl, siteId, branchId, opts) {
|
|
|
121
68
|
schemas[registryComponentKey(schema.name)] = schema;
|
|
122
69
|
}
|
|
123
70
|
catch {
|
|
124
|
-
// Skip components that fail to fetch — don't block the rest
|
|
125
71
|
}
|
|
126
72
|
}));
|
|
127
73
|
cache.set(key, { cachedAt: Date.now(), schemas });
|
|
128
74
|
return schemas;
|
|
129
75
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* Useful for cache invalidation checks — compare IDs against a local cache
|
|
133
|
-
* to detect which component schemas have been updated without fetching bodies.
|
|
134
|
-
*
|
|
135
|
-
* Note: the CSS list endpoint does not currently expose versionId; the document
|
|
136
|
-
* id is returned as a stable identifier. Full version-id tracking requires a
|
|
137
|
-
* backend extension (tracked separately).
|
|
138
|
-
*/
|
|
139
|
-
export async function listRegistryVersions(cssBaseUrl, siteId, branchId, opts) {
|
|
140
|
-
const base = cssBaseUrl.replace(/\/$/, '');
|
|
76
|
+
export async function listRegistryVersions(ccrBaseUrl, siteId, branchId, opts) {
|
|
77
|
+
const base = ccrBaseUrl.replace(/\/$/, '');
|
|
141
78
|
const headers = {
|
|
142
79
|
'Content-Type': 'application/json',
|
|
143
80
|
'X-API-Key': opts.token,
|
|
@@ -151,6 +88,6 @@ export async function listRegistryVersions(cssBaseUrl, siteId, branchId, opts) {
|
|
|
151
88
|
const { documents } = (await res.json());
|
|
152
89
|
return documents.map((doc) => ({
|
|
153
90
|
name: componentNameFromPath(doc.path),
|
|
154
|
-
versionId: doc.id,
|
|
91
|
+
versionId: doc.id,
|
|
155
92
|
}));
|
|
156
93
|
}
|
|
@@ -35,7 +35,6 @@ function pinMapOf(template) {
|
|
|
35
35
|
}
|
|
36
36
|
return isPlainObject(props._pinMap) ? props._pinMap : {};
|
|
37
37
|
}
|
|
38
|
-
// A pinned slot is a component with a string props.id whose _pinMap entry is strictly true.
|
|
39
38
|
function pinnedSlots(list, pinMap) {
|
|
40
39
|
const slots = [];
|
|
41
40
|
for (const component of list) {
|
|
@@ -46,7 +45,6 @@ function pinnedSlots(list, pinMap) {
|
|
|
46
45
|
}
|
|
47
46
|
return slots;
|
|
48
47
|
}
|
|
49
|
-
// Document content is the top-level content[]; when absent it falls back to root.props.content.
|
|
50
48
|
function documentContentOf(documentSnapshot) {
|
|
51
49
|
if (!isPlainObject(documentSnapshot)) {
|
|
52
50
|
return [];
|
|
@@ -80,8 +78,6 @@ function indexOfId(list, id) {
|
|
|
80
78
|
}
|
|
81
79
|
return -1;
|
|
82
80
|
}
|
|
83
|
-
// Within one list, pinned slots must keep their template-relative order. A slot absent
|
|
84
|
-
// from this document list (present elsewhere or missing) neither errors nor advances the chain.
|
|
85
81
|
function checkListOrder(slots, documentList, errors) {
|
|
86
82
|
let lastFoundIndex = -1;
|
|
87
83
|
slots.forEach((slot, expectedIndex) => {
|
|
@@ -103,39 +99,10 @@ function checkListOrder(slots, documentList, errors) {
|
|
|
103
99
|
lastFoundIndex = actualIndex;
|
|
104
100
|
});
|
|
105
101
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Validates a document snapshot against a content-shaped template by slot-id membership.
|
|
108
|
-
*
|
|
109
|
-
* A template component is a pinned slot when it has a string `props.id` and
|
|
110
|
-
* `root.props._pinMap[id]` is strictly `true`. Slots are read from the template's
|
|
111
|
-
* `content[]` and each `zones[key][]`. A document conforms when:
|
|
112
|
-
*
|
|
113
|
-
* 1. Presence: every pinned slot id appears among the document's component ids,
|
|
114
|
-
* collected from the top-level `content[]` (or `root.props.content` when the
|
|
115
|
-
* top-level array is absent) and every `zones[key][]`.
|
|
116
|
-
* 2. Order: within each list, the pinned slots found in that list keep the template's
|
|
117
|
-
* relative order. A pinned slot found in a different list than the template placed it
|
|
118
|
-
* in raises no order error and does not advance that list's order chain.
|
|
119
|
-
*
|
|
120
|
-
* Matching is by id, so a same-typed local component never satisfies a pinned slot and a
|
|
121
|
-
* duplicated type cannot mask a missing one. A template that is not a content-shaped
|
|
122
|
-
* snapshot (missing `content` array, malformed `root`/`_pinMap`, or the legacy
|
|
123
|
-
* `{ components }` manifest) pins nothing, and every document conforms.
|
|
124
|
-
*
|
|
125
|
-
* Never throws: every property access is type-guarded, and malformed input yields an
|
|
126
|
-
* empty error list rather than an exception.
|
|
127
|
-
*
|
|
128
|
-
* @param input - Document snapshot and the template snapshot to validate against
|
|
129
|
-
* @returns Object with an array of structural conformance errors (empty when valid)
|
|
130
|
-
*/
|
|
131
102
|
export function validateDocumentStructure(input) {
|
|
132
103
|
const { documentSnapshot, templateSnapshot } = input;
|
|
133
104
|
const errors = [];
|
|
134
|
-
// Only a content-shaped snapshot pins slots; anything else conforms unconditionally.
|
|
135
105
|
if (!isPlainObject(templateSnapshot) || !Array.isArray(templateSnapshot.content)) {
|
|
136
|
-
// A template bound to live documents that is not content-shaped disables
|
|
137
|
-
// structural validation for all of them; surface it so operators can catch
|
|
138
|
-
// a broken template deployment rather than have every document silently pass.
|
|
139
106
|
console.warn('[p1-content-validator] Template snapshot is not content-shaped; skipping structural validation and treating the document as conforming.');
|
|
140
107
|
return { errors };
|
|
141
108
|
}
|
package/dist/validator.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { registryComponentKey } from './registry.js';
|
|
2
2
|
import { isComponentShape, resolvePropPath } from './guards.js';
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Internal helpers
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
7
3
|
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
8
|
-
// Puck type-prefixed: {ComponentType}-{uuid-v4}
|
|
9
4
|
const PREFIXED_UUID_RE = /^[A-Za-z][A-Za-z0-9]*-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10
|
-
// ULID: 26 Crockford base32 chars (legacy — MCP server previously generated these)
|
|
11
5
|
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
12
6
|
function isValidPuckId(value) {
|
|
13
7
|
if (typeof value !== 'string' || value === '')
|
|
@@ -22,21 +16,15 @@ function allowedPropsForSchema(schema) {
|
|
|
22
16
|
...(schema.allowedAdditionalProps ?? []),
|
|
23
17
|
]);
|
|
24
18
|
}
|
|
25
|
-
/** Returns the field definition for a prop key, or undefined if not found. */
|
|
26
19
|
function findField(schema, propKey) {
|
|
27
20
|
return schema.fields?.find((f) => f.name === propKey);
|
|
28
21
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Validates a prop value against a select/radio field's allowed options.
|
|
31
|
-
* Only fires when the field has options defined and the value is a string.
|
|
32
|
-
*/
|
|
33
22
|
function validateEnumValue(field, value, componentType, propKey, opIndex, path, errors) {
|
|
34
23
|
if ((field.type !== 'select' && field.type !== 'radio') ||
|
|
35
24
|
field.options === undefined ||
|
|
36
25
|
field.options.length === 0) {
|
|
37
26
|
return;
|
|
38
27
|
}
|
|
39
|
-
// Only validate primitives — skip array/object values (e.g. stats array items)
|
|
40
28
|
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
41
29
|
return;
|
|
42
30
|
const allowedValues = field.options.map((o) => o.value);
|
|
@@ -51,7 +39,6 @@ function validateEnumValue(field, value, componentType, propKey, opIndex, path,
|
|
|
51
39
|
}
|
|
52
40
|
}
|
|
53
41
|
function validateComponent(comp, registry, opIndex, path, errors, zonesKey, warnOnZonesUsage) {
|
|
54
|
-
// readOnly is a Puck runtime-managed sibling — writers must not set it
|
|
55
42
|
if ('readOnly' in comp) {
|
|
56
43
|
errors.push({
|
|
57
44
|
opIndex,
|
|
@@ -60,9 +47,6 @@ function validateComponent(comp, registry, opIndex, path, errors, zonesKey, warn
|
|
|
60
47
|
message: `"readOnly" at "${path}" is a Puck runtime field and must not be set by writers.`,
|
|
61
48
|
});
|
|
62
49
|
}
|
|
63
|
-
// Every Puck component must have a valid id in its props — it is how the
|
|
64
|
-
// editor tracks the component instance. Accept UUID v4, type-prefixed UUID v4,
|
|
65
|
-
// or ULID; reject arbitrary strings like "roger".
|
|
66
50
|
if (!('id' in comp.props)) {
|
|
67
51
|
errors.push({
|
|
68
52
|
opIndex,
|
|
@@ -90,27 +74,6 @@ function validateComponent(comp, registry, opIndex, path, errors, zonesKey, warn
|
|
|
90
74
|
});
|
|
91
75
|
return;
|
|
92
76
|
}
|
|
93
|
-
// The descriptor's own `name` is the source of truth for a component type's
|
|
94
|
-
// casing. The lookup above is case-insensitive on purpose, and must stay that
|
|
95
|
-
// way: it is how a mis-cased type gets *found* so the error below can name the
|
|
96
|
-
// casing the writer should have used. Making it exact would degrade this into
|
|
97
|
-
// a bare unknown_component_type and lose the actionable hint.
|
|
98
|
-
//
|
|
99
|
-
// Matching loosely is not licence to write loosely. Puck resolves `type` by
|
|
100
|
-
// exact key lookup into config.components with no normalisation and no guard
|
|
101
|
-
// (verified in @puckeditor/core@0.21.1 — chunk-EBISZQTK.mjs:4124 reads
|
|
102
|
-
// `config.components[item.type]` then dereferences `.fields` unguarded, so a
|
|
103
|
-
// miss throws), which means a document holding "quoteblock" against a config
|
|
104
|
-
// keyed "QuoteBlock" breaks the editor and the published page even though
|
|
105
|
-
// every prop on it validated fine.
|
|
106
|
-
//
|
|
107
|
-
// Reject rather than silently canonicalise: a writer that meant a different
|
|
108
|
-
// component should hear about it, and naming the expected casing makes the fix
|
|
109
|
-
// mechanical.
|
|
110
|
-
//
|
|
111
|
-
// `schema.name` is required for in-repo callers but validateOps is a public
|
|
112
|
-
// boundary — a hand-built registry may omit it, and there is no canonical
|
|
113
|
-
// casing to hold the writer to in that case.
|
|
114
77
|
if (typeof schema.name === 'string' && schema.name !== '' && comp.type !== schema.name) {
|
|
115
78
|
errors.push({
|
|
116
79
|
opIndex,
|
|
@@ -131,15 +94,13 @@ function validateComponent(comp, registry, opIndex, path, errors, zonesKey, warn
|
|
|
131
94
|
message: `Unknown prop "${key}" on "${comp.type}" at "${path}.props". ` +
|
|
132
95
|
`Allowed: ${[...allowedKeys].join(', ')}.`,
|
|
133
96
|
});
|
|
134
|
-
continue;
|
|
97
|
+
continue;
|
|
135
98
|
}
|
|
136
|
-
// Validate enum values for select/radio fields
|
|
137
99
|
const field = findField(schema, key);
|
|
138
100
|
if (field !== undefined) {
|
|
139
101
|
validateEnumValue(field, value, comp.type, key, opIndex, `${path}.props.${key}`, errors);
|
|
140
102
|
}
|
|
141
103
|
}
|
|
142
|
-
// Recurse into slot props: non-opaque array props containing component shapes
|
|
143
104
|
const opaqueProps = new Set(schema.opaqueProps ?? []);
|
|
144
105
|
for (const [key, val] of Object.entries(comp.props)) {
|
|
145
106
|
if (opaqueProps.has(key) || key === 'id')
|
|
@@ -162,7 +123,6 @@ function validateContent(value, registry, opIndex, path, errors, zonesKey, warnO
|
|
|
162
123
|
validateComponent(value, registry, opIndex, path, errors, zonesKey, warnOnZonesUsage);
|
|
163
124
|
return;
|
|
164
125
|
}
|
|
165
|
-
// Plain object (e.g., whole-document or sub-document): walk its keys
|
|
166
126
|
for (const [key, val] of Object.entries(value)) {
|
|
167
127
|
const childPath = path !== '' ? `${path}.${key}` : key;
|
|
168
128
|
if (warnOnZonesUsage && key === zonesKey) {
|
|
@@ -176,13 +136,6 @@ function validateContent(value, registry, opIndex, path, errors, zonesKey, warnO
|
|
|
176
136
|
validateContent(val, registry, opIndex, childPath, errors, zonesKey, warnOnZonesUsage);
|
|
177
137
|
}
|
|
178
138
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Validates a targeted prop write (e.g. content.2.props.background = "roger")
|
|
181
|
-
* using the current document snapshot to resolve the component type at the
|
|
182
|
-
* parent path. Checks both the prop key and the value against the registry schema.
|
|
183
|
-
*
|
|
184
|
-
* Only fires when the op path contains ".props." and a snapshot is available.
|
|
185
|
-
*/
|
|
186
139
|
function validatePropPathOp(op, opIndex, snapshot, registry, errors) {
|
|
187
140
|
const resolved = resolvePropPath(op.path, snapshot);
|
|
188
141
|
if (resolved === undefined)
|
|
@@ -190,15 +143,7 @@ function validatePropPathOp(op, opIndex, snapshot, registry, errors) {
|
|
|
190
143
|
const { component: val, componentPath, propsIdx, parts } = resolved;
|
|
191
144
|
const schema = registry[registryComponentKey(val.type)];
|
|
192
145
|
if (!schema)
|
|
193
|
-
return;
|
|
194
|
-
// Deliberately no case-mismatch check here: `val.type` is read from the
|
|
195
|
-
// stored snapshot, not from this op. A mis-cased type in the document is
|
|
196
|
-
// damage some earlier write already did, and erroring on it would make the
|
|
197
|
-
// document unrepairable — the prop edits that fix it would themselves be
|
|
198
|
-
// rejected. Casing is enforced where a type actually enters the document
|
|
199
|
-
// (validateComponent).
|
|
200
|
-
// Case A: path ends exactly at .props — content is the full props object
|
|
201
|
-
// e.g. replace content.0.props { id, label, visible }
|
|
146
|
+
return;
|
|
202
147
|
if (propsIdx === parts.length - 1) {
|
|
203
148
|
if (op.content === null || typeof op.content !== 'object' || Array.isArray(op.content))
|
|
204
149
|
return;
|
|
@@ -241,10 +186,7 @@ function validatePropPathOp(op, opIndex, snapshot, registry, errors) {
|
|
|
241
186
|
}
|
|
242
187
|
return;
|
|
243
188
|
}
|
|
244
|
-
// Case B: path goes through .props.KEY — targeted single-prop write
|
|
245
|
-
// e.g. replace content.0.props.background "roger"
|
|
246
189
|
const propKey = parts[propsIdx + 1];
|
|
247
|
-
// id is always an allowed key but validate its value format
|
|
248
190
|
if (propKey === 'id') {
|
|
249
191
|
if (!isValidPuckId(op.content)) {
|
|
250
192
|
errors.push({
|
|
@@ -272,21 +214,11 @@ function validatePropPathOp(op, opIndex, snapshot, registry, errors) {
|
|
|
272
214
|
validateEnumValue(field, op.content, val.type, propKey, opIndex, op.path, errors);
|
|
273
215
|
}
|
|
274
216
|
}
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// Public API
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
217
|
export function validateOps(input) {
|
|
279
218
|
const { operations, registry, currentSnapshot, config = {} } = input;
|
|
280
|
-
// Graceful degradation: if registry is empty skip all validation.
|
|
281
219
|
if (Object.keys(registry).length === 0) {
|
|
282
220
|
return { errors: [] };
|
|
283
221
|
}
|
|
284
|
-
// Defense-in-depth: re-key the registry by its case-insensitive lookup key,
|
|
285
|
-
// regardless of what casing the caller's keys already use. Registry
|
|
286
|
-
// producers (fetchRegistry / McpApiClient.fetchRegistrySchemas) already key
|
|
287
|
-
// by registryComponentKey, but validateOps normalizes independently so a
|
|
288
|
-
// caller-supplied registry (e.g. a hand-built one, or one that changes its
|
|
289
|
-
// convention later) can't silently reintroduce case-sensitive misses.
|
|
290
222
|
const normalizedRegistry = {};
|
|
291
223
|
for (const [key, schema] of Object.entries(registry)) {
|
|
292
224
|
normalizedRegistry[registryComponentKey(key)] = schema;
|
|
@@ -317,12 +249,9 @@ export function validateOps(input) {
|
|
|
317
249
|
message: `"${zonesKey}" is deprecated. Use slot props instead.`,
|
|
318
250
|
});
|
|
319
251
|
}
|
|
320
|
-
// Snapshot-based validation: catches targeted prop writes where the content
|
|
321
|
-
// is a primitive and the component type must be resolved from the live document.
|
|
322
252
|
if (currentSnapshot !== undefined) {
|
|
323
253
|
validatePropPathOp(op, opIndex, currentSnapshot, normalizedRegistry, errors);
|
|
324
254
|
}
|
|
325
|
-
// Content-shape validation: catches component replacements and slot content.
|
|
326
255
|
validateContent(op.content, normalizedRegistry, opIndex, op.path, errors, zonesKey, warnOnZonesUsage);
|
|
327
256
|
}
|
|
328
257
|
return { errors };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon-systems/p1-content-validator",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Validates Puck component edit operations against the
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Validates Puck component edit operations against the P1 component registry",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/pantheon-systems/p1-platform.git",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "UNLICENSED",
|
|
37
37
|
"homepage": "https://github.com/pantheon-systems/p1-platform/tree/main/packages/p1-content-validator",
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "tsc",
|
|
39
|
+
"build": "pnpm run clean && tsc --emitDeclarationOnly && tsc --declaration false --declarationMap false --removeComments",
|
|
40
40
|
"clean": "rm -rf dist *.tsbuildinfo",
|
|
41
41
|
"lint": "eslint src tests",
|
|
42
42
|
"lint:fix": "eslint src tests --fix",
|