@decocms/blocks-cli 7.37.5 → 7.37.6

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-cli",
3
- "version": "7.37.5",
3
+ "version": "7.37.6",
4
4
  "type": "module",
5
5
  "description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "lint:unused": "knip"
32
32
  },
33
33
  "dependencies": {
34
- "@decocms/blocks": "7.37.5",
34
+ "@decocms/blocks": "7.37.6",
35
35
  "ts-morph": "^27.0.0",
36
36
  "tsx": "^4.22.5"
37
37
  },
@@ -191,6 +191,68 @@ describe("typeToJsonSchema with intersection types", () => {
191
191
  }, 30_000);
192
192
  });
193
193
 
194
+ describe("typeToJsonSchema Section-typed props", () => {
195
+ // The framework's `Section` is opaque (`export type Section = any`), so a
196
+ // Section-typed prop is a "pick any section" reference emitted as a
197
+ // __SECTION_REF__ picker. This must keep working.
198
+ it("emits a __SECTION_REF__ picker for the framework's opaque Section[]", () => {
199
+ const project = new Project({
200
+ useInMemoryFileSystem: true,
201
+ compilerOptions: { skipLibCheck: true },
202
+ });
203
+ const sf = project.createSourceFile(
204
+ "props.ts",
205
+ `
206
+ type Section = any;
207
+ export interface Props {
208
+ children?: Section[] | null;
209
+ }
210
+ `,
211
+ );
212
+ const schema = typeToJsonSchema(sf.getInterfaceOrThrow("Props").getType());
213
+
214
+ expect(schema.properties.children).toMatchObject({
215
+ type: "array",
216
+ items: { $ref: "#/definitions/__SECTION_REF__" },
217
+ });
218
+ }, 30_000);
219
+
220
+ // Regression (casaevideo footer): a component that declares its own local
221
+ // `type Section = { label; items }` used to collide with the magic name and
222
+ // collapse into a non-editable __SECTION_REF__ picker — the footer columns
223
+ // showed only a drag handle, no expand arrow, no form. A concretely-shaped
224
+ // local type is user data and must render as an inline editable object.
225
+ it("keeps a user-defined local `type Section` as an inline editable object", () => {
226
+ const project = new Project({
227
+ useInMemoryFileSystem: true,
228
+ compilerOptions: { skipLibCheck: true },
229
+ });
230
+ const sf = project.createSourceFile(
231
+ "props.ts",
232
+ `
233
+ interface Item { label: string; href: string; }
234
+ type Section = { label: string; items: Item[] };
235
+ export interface Props {
236
+ sections?: Section[];
237
+ }
238
+ `,
239
+ );
240
+ const schema = typeToJsonSchema(sf.getInterfaceOrThrow("Props").getType());
241
+
242
+ const sections = schema.properties.sections;
243
+ // Editable array of objects — NOT a section-picker reference.
244
+ expect(sections.type).toBe("array");
245
+ expect(JSON.stringify(sections)).not.toContain("__SECTION_REF__");
246
+
247
+ const item = sections.items;
248
+ expect(item.type).toBe("object");
249
+ expect(item.properties.label).toMatchObject({ type: "string" });
250
+ expect(item.properties.items.type).toBe("array");
251
+ expect(item.properties.items.items.type).toBe("object");
252
+ expect(item.properties.items.items.properties.href).toMatchObject({ type: "string" });
253
+ }, 30_000);
254
+ });
255
+
194
256
  describe("typeToJsonSchema loader block-ref matching", () => {
195
257
  // Regression: a loader that returns an array whose element type has no
196
258
  // resolvable name (`VNode[]`, `string[]`, …) used to be bucketed under the
@@ -306,6 +306,33 @@ const SECTION_REF_DEF_KEY = "__SECTION_REF__";
306
306
  // Well-known definition key for Resolvable (saved blocks picker)
307
307
  const RESOLVABLE_KEY = "Resolvable";
308
308
 
309
+ /**
310
+ * Whether a prop annotated `Section` / `Section[]` is the framework's opaque
311
+ * Section type (a "pick any section" reference) rather than a user-defined type
312
+ * that merely happens to be named `Section`.
313
+ *
314
+ * The framework's `Section` is opaque — `export type Section = any` in the
315
+ * scaffolded `~/types/deco.ts` — so it resolves to `any` (or `unknown`). A
316
+ * component that declares its own local `type Section = { label; items }`
317
+ * (e.g. a footer column list) resolves to a concrete object with properties;
318
+ * that is user data and must render as an inline editable object, NOT a picker.
319
+ *
320
+ * Discriminates by the resolved shape after stripping `| null | undefined` and
321
+ * the array wrapper: only the opaque type is a section reference.
322
+ */
323
+ function isOpaqueSectionType(propType: Type): boolean {
324
+ let type = propType;
325
+ if (type.isUnion()) {
326
+ const nonNull = type.getUnionTypes().filter((u) => !u.isNull() && !u.isUndefined());
327
+ if (nonNull.length === 1) type = nonNull[0];
328
+ }
329
+ if (type.isArray()) {
330
+ const el = type.getArrayElementType();
331
+ if (el) type = el;
332
+ }
333
+ return type.isAny() || type.isUnknown();
334
+ }
335
+
309
336
  // Only truly React-internal props that are never user-defined.
310
337
  // Do NOT include "children", "type", "props", or "key" — those are commonly
311
338
  // used as legitimate property names in data interfaces (e.g. SelectedFacet
@@ -560,9 +587,16 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
560
587
  if (tn) typeHint = tn.getText();
561
588
  }
562
589
 
563
- // Section type → section picker reference (resolved by composeMeta)
590
+ // Section type → section picker reference (resolved by composeMeta).
591
+ // Guard on the resolved shape: the name `Section` is not reserved, so a
592
+ // component may declare its own local `type Section = { label; items }`
593
+ // (e.g. a footer column list). Only the framework's opaque Section type
594
+ // becomes a picker; a concretely-shaped local type falls through to the
595
+ // normal inline-object handling below (`isOpaqueSectionType`).
564
596
  const baseHint = typeHint.replace(/\s*\|\s*(null|undefined)/g, "").trim();
565
- if (baseHint === "Section" || baseHint === "Section[]" || baseHint === "Section[] | null") {
597
+ const isSectionName =
598
+ baseHint === "Section" || baseHint === "Section[]" || baseHint === "Section[] | null";
599
+ if (isSectionName && isOpaqueSectionType(propType)) {
566
600
  const isArray = baseHint.includes("[]");
567
601
  const sectionSchema: any = isArray
568
602
  ? {