@decocms/blocks 7.26.3 → 7.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks",
3
- "version": "7.26.3",
3
+ "version": "7.27.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -253,9 +253,10 @@ describe("commerce SEO section schemas", () => {
253
253
  // PLP-specific structured-data config (see seo-form-mode.ts PLP_SEO_FIELD_KEYS).
254
254
  expect(props.configJsonLD.properties).toHaveProperty("removeVideos");
255
255
  expect(props.configJsonLD.properties).toHaveProperty("ignoreStructuredData");
256
- // The data source is set once at creation and stays hidden in the SEO panel;
257
- // it round-trips via the form rather than being re-picked here.
258
- expect(props.jsonLD.hide).toBe(true);
256
+ // The data source is a loader picker (anyOf over the loader union), not
257
+ // hidden — the user picks the PLP loader that feeds the structured data.
258
+ expect(Array.isArray(props.jsonLD.anyOf)).toBe(true);
259
+ expect(props.jsonLD.title).toBe("Data Source");
259
260
  });
260
261
 
261
262
  it("emits editable props for the product-details SEO section (pdp mode)", () => {
@@ -268,7 +269,8 @@ describe("commerce SEO section schemas", () => {
268
269
  // PDP-specific fields (see seo-form-mode.ts PDP_SEO_FIELD_KEYS).
269
270
  expect(props).toHaveProperty("omitVariants");
270
271
  expect(props).toHaveProperty("ignoreStructuredData");
271
- expect(props.jsonLD.hide).toBe(true);
272
+ expect(Array.isArray(props.jsonLD.anyOf)).toBe(true);
273
+ expect(props.jsonLD.title).toBe("Data Source");
272
274
  });
273
275
 
274
276
  it("registers the defs + manifest blocks and offers them as page.seo options", () => {
@@ -285,6 +287,51 @@ describe("commerce SEO section schemas", () => {
285
287
  expect(meta.schema.root.sections.anyOf).toContainEqual(b64Ref(key));
286
288
  }
287
289
  });
290
+
291
+ // Mirrors a generated site meta: a loader carries a real def AND is listed in
292
+ // the site's loader union. `@ignore`d loaders are the exception — they get a
293
+ // def but are withheld from the union.
294
+ function siteMetaWithLoader(key: string, opts: { inUnion: boolean }) {
295
+ const site = emptySiteMeta();
296
+ site.schema.definitions[b64(key)] = {
297
+ title: key,
298
+ type: "object",
299
+ properties: { __resolveType: { type: "string", enum: [key] } },
300
+ };
301
+ site.manifest.blocks.loaders = {
302
+ [key]: { $ref: `#/definitions/${b64(key)}`, namespace: "site" },
303
+ };
304
+ site.schema.root = { loaders: { anyOf: opts.inUnion ? [b64Ref(key)] : [] } };
305
+ return site;
306
+ }
307
+
308
+ it("feeds the site's own loaders into the jsonLD data-source picker", () => {
309
+ // The runtime loader registry is empty at generation time, so the union must
310
+ // be seeded from the site's own loader union — else the picker has no
311
+ // options and the user can't wire the data source.
312
+ const loaderKey = "site/loaders/product/productDetailsPage.ts";
313
+ const meta = composeMeta(siteMetaWithLoader(loaderKey, { inUnion: true }));
314
+
315
+ // The picker option is real, not a dangling $ref: its def survives.
316
+ expect(meta.schema.definitions).toHaveProperty(b64(loaderKey));
317
+ // …offered in both the root union and the commerce SEO jsonLD picker.
318
+ expect(meta.schema.root.loaders.anyOf).toContainEqual(b64Ref(loaderKey));
319
+ const seoDef =
320
+ meta.schema.definitions[b64("commerce/sections/Seo/SeoPDPV2.tsx")];
321
+ expect(seoDef.properties.jsonLD.anyOf).toContainEqual(b64Ref(loaderKey));
322
+ });
323
+
324
+ it("keeps @ignore'd loaders (in the manifest, not the union) out of the picker", () => {
325
+ // Seeding from `manifest.blocks.loaders` would resurrect hidden loaders;
326
+ // seeding from the union keeps them out.
327
+ const hiddenKey = "site/loaders/internal/hidden.ts";
328
+ const meta = composeMeta(siteMetaWithLoader(hiddenKey, { inUnion: false }));
329
+
330
+ expect(meta.schema.root.loaders.anyOf).not.toContainEqual(b64Ref(hiddenKey));
331
+ const seoDef =
332
+ meta.schema.definitions[b64("commerce/sections/Seo/SeoPDPV2.tsx")];
333
+ expect(seoDef.properties.jsonLD.anyOf).not.toContainEqual(b64Ref(hiddenKey));
334
+ });
288
335
  });
289
336
 
290
337
  describe("composeMeta idempotency", () => {
package/src/cms/schema.ts CHANGED
@@ -731,7 +731,7 @@ function buildPageSchema(sectionAnyOf: any[]) {
731
731
  // Framework sections
732
732
  // ---------------------------------------------------------------------------
733
733
 
734
- function buildFrameworkSections(sectionAnyOf: any[]) {
734
+ function buildFrameworkSections(sectionAnyOf: any[], loaderUnion: any[]) {
735
735
  const definitions: Record<string, any> = {};
736
736
  const manifestBlocks: Record<string, any> = {};
737
737
  const extraAnyOf: any[] = [];
@@ -839,8 +839,12 @@ function buildFrameworkSections(sectionAnyOf: any[]) {
839
839
  // there is no manifest to emit these props from. The Studio's SEO editor
840
840
  // already knows this type as its "plp" mode (see seo-form-mode.ts); it just
841
841
  // needs the props schema to render the override fields. `jsonLD` is the
842
- // data-source block-ref set once when the page is created; it round-trips via
843
- // the form (kept `hide`) rather than being re-picked in the SEO panel.
842
+ // data-source loader picker (mirrors deco-cx's `@title Data Source`) — an
843
+ // inline anyOf over the loader union so the editor lets you pick the PLP/PDP
844
+ // loader that feeds the structured data. Inlined (a fresh copy) rather than a
845
+ // `$ref: "#/root/loaders"` because the Studio drops the prop-level title when
846
+ // resolving a `$ref` union, which would lose the "Data Source" label; the
847
+ // copy also avoids sharing one array instance across the two defs + root.
844
848
  //
845
849
  // Added to `extraAnyOf` (offered as a selectable page.seo type) unconditionally,
846
850
  // like the website Seo sections above. Gating this on "is a commerce site" via
@@ -861,12 +865,7 @@ function buildFrameworkSections(sectionAnyOf: any[]) {
861
865
  enum: [SEO_PLP_V2_TYPE],
862
866
  default: SEO_PLP_V2_TYPE,
863
867
  },
864
- jsonLD: {
865
- type: "object",
866
- title: "Data Source",
867
- additionalProperties: true,
868
- hide: true,
869
- },
868
+ jsonLD: { title: "Data Source", anyOf: [...loaderUnion] },
870
869
  title: { type: "string", title: "Title Override" },
871
870
  description: { type: "string", title: "Description Override" },
872
871
  noIndexing: { type: "boolean", title: "Disable indexing" },
@@ -900,12 +899,7 @@ function buildFrameworkSections(sectionAnyOf: any[]) {
900
899
  enum: [SEO_PDP_V2_TYPE],
901
900
  default: SEO_PDP_V2_TYPE,
902
901
  },
903
- jsonLD: {
904
- type: "object",
905
- title: "Data Source",
906
- additionalProperties: true,
907
- hide: true,
908
- },
902
+ jsonLD: { title: "Data Source", anyOf: [...loaderUnion] },
909
903
  omitVariants: { type: "boolean", title: "Omit variants" },
910
904
  title: { type: "string", title: "Title Override" },
911
905
  description: { type: "string", title: "Description Override" },
@@ -1109,11 +1103,36 @@ export function composeMeta(
1109
1103
 
1110
1104
  const siteAnyOf = siteMeta.schema?.root?.sections?.anyOf || [];
1111
1105
 
1112
- // Build all framework components
1113
- const fwSections = buildFrameworkSections(siteAnyOf);
1106
+ // Build all framework components. Loaders first: the commerce SEO sections'
1107
+ // `jsonLD` data-source picker references the loader union, and that union must
1108
+ // include the site's OWN loaders. The runtime registry (loaderRegistry) is
1109
+ // empty at generation time, so `buildLoaderDefinitions` alone yields just
1110
+ // `Resolvable` — merge in the site's own loaders so loader-backed pickers have
1111
+ // real options in the baked meta, not only at runtime.
1112
+ //
1113
+ // Seed from the site's loader UNION (`schema.root.loaders.anyOf`), NOT
1114
+ // `manifest.blocks.loaders`: the generator keeps `@ignore`d loaders in the
1115
+ // manifest (they still need a def) while withholding them from the union so
1116
+ // they can't be picked — reading the manifest would resurrect them. This
1117
+ // source is also already `{ $ref }`-shaped and only points at defs that exist
1118
+ // (no dangling refs).
1119
+ //
1120
+ // TODO: `root.matchers`/`root.actions` have the same empty-union-at-gen-time
1121
+ // gap; only loaders are merged here (commerce SEO scope). Generalize if a
1122
+ // matcher/action picker ever needs baked options.
1123
+ const loaders = buildLoaderDefinitions();
1124
+ const siteLoaderRefs = (siteMeta.schema?.root?.loaders?.anyOf ?? []).filter(
1125
+ (ref: any): ref is { $ref: string } => Boolean(ref) && typeof ref.$ref === "string",
1126
+ );
1127
+ const seenLoaderRefs = new Set<string>(loaders.loaderAnyOf.map((r: any) => r.$ref));
1128
+ const loaderUnion = [
1129
+ ...loaders.loaderAnyOf,
1130
+ ...siteLoaderRefs.filter((r) => !seenLoaderRefs.has(r.$ref)),
1131
+ ];
1132
+
1133
+ const fwSections = buildFrameworkSections(siteAnyOf, loaderUnion);
1114
1134
  const fullSectionAnyOf = [...siteAnyOf, ...fwSections.extraAnyOf];
1115
1135
  const page = buildPageSchema(fullSectionAnyOf);
1116
- const loaders = buildLoaderDefinitions();
1117
1136
  const actions = buildActionDefinitions();
1118
1137
  const matchers = buildMatcherDefinitions();
1119
1138
 
@@ -1174,7 +1193,7 @@ export function composeMeta(
1174
1193
  ...(siteMeta.schema?.root || {}),
1175
1194
  sections: { anyOf: fullSectionAnyOf },
1176
1195
  pages: { anyOf: page.rootAnyOf },
1177
- loaders: { anyOf: loaders.loaderAnyOf },
1196
+ loaders: { anyOf: loaderUnion },
1178
1197
  matchers: { anyOf: matchers.matcherAnyOf },
1179
1198
  },
1180
1199
  },