@decocms/blocks-cli 7.17.0 → 7.17.1

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.17.0",
3
+ "version": "7.17.1",
4
4
  "type": "module",
5
5
  "description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "lint:unused": "knip"
31
31
  },
32
32
  "dependencies": {
33
- "@decocms/blocks": "7.17.0",
33
+ "@decocms/blocks": "7.17.1",
34
34
  "ts-morph": "^27.0.0",
35
35
  "tsx": "^4.22.5"
36
36
  },
@@ -157,6 +157,66 @@ describe("typeToJsonSchema with intersection types", () => {
157
157
  }, 30_000);
158
158
  });
159
159
 
160
+ describe("typeToJsonSchema loader block-ref matching", () => {
161
+ // Regression: a loader that returns an array whose element type has no
162
+ // resolvable name (`VNode[]`, `string[]`, …) used to be bucketed under the
163
+ // generic key "Array". Because an array type's OWN symbol name is also
164
+ // "Array", EVERY array-of-objects section prop (`Collection[]`, `Tab[]`)
165
+ // matched that bucket and collapsed into a block-ref picker — so the array
166
+ // was no longer editable and its item fields (label, nested loaders) all
167
+ // disappeared from the CMS form. The two guards (drop the "Array" bucket at
168
+ // registration; never look it up from an array prop's symbol name) keep the
169
+ // array inline and editable.
170
+ it("does not collapse an array-of-objects prop into a block-ref via the generic 'Array' bucket", () => {
171
+ const project = new Project({
172
+ useInMemoryFileSystem: true,
173
+ compilerOptions: { skipLibCheck: true },
174
+ });
175
+ const sf = project.createSourceFile(
176
+ "props.ts",
177
+ `
178
+ interface Product { productID: string; name: string; }
179
+ interface Collection { label: string; products: Product[] | null; }
180
+ export interface Props {
181
+ collections: Collection[];
182
+ topLevelProducts?: Product[] | null;
183
+ }
184
+ `,
185
+ );
186
+ const ctx = {
187
+ // Mimics a site where List/Sections (VNode[]) and skuList (string[])
188
+ // would otherwise land in an "Array" bucket, plus a real Product[] loader.
189
+ outputTypeToLoaderKeys: new Map<string, string[]>([
190
+ ["Product[]", ["site/loaders/algolia/products/list.ts"]],
191
+ ["Array", ["site/loaders/List/Sections.tsx", "site/loaders/skuList.ts"]],
192
+ ]),
193
+ };
194
+
195
+ const schema = typeToJsonSchema(sf.getInterfaceOrThrow("Props").getType(), new Set(), ctx);
196
+ const collections = schema.properties.collections;
197
+
198
+ // Stays an editable array of objects — NOT a block-ref anyOf.
199
+ expect(collections.type).toBe("array");
200
+ expect(collections.anyOf).toBeUndefined();
201
+
202
+ const item = collections.items;
203
+ expect(item.type).toBe("object");
204
+ expect(item.properties.label).toMatchObject({ type: "string" });
205
+ // The nested Product[] field still resolves to a loader picker (its type
206
+ // name matches a registered loader output).
207
+ expect(item.properties.products.anyOf).toEqual([
208
+ { $ref: "#/definitions/Resolvable" },
209
+ { $ref: "#/definitions/c2l0ZS9sb2FkZXJzL2FsZ29saWEvcHJvZHVjdHMvbGlzdC50cw==" },
210
+ ]);
211
+
212
+ // Control: a top-level Product[] prop still resolves to a loader picker.
213
+ expect(schema.properties.topLevelProducts.anyOf).toEqual([
214
+ { $ref: "#/definitions/Resolvable" },
215
+ { $ref: "#/definitions/c2l0ZS9sb2FkZXJzL2FsZ29saWEvcHJvZHVjdHMvbGlzdC50cw==" },
216
+ ]);
217
+ }, 30_000);
218
+ });
219
+
160
220
  // ---------------------------------------------------------------------------
161
221
  // Default output path (.deco/) + legacy warning — subprocess, mirrors the
162
222
  // pattern in generate-sections.test.ts. generate-schema.ts IS guarded by
@@ -345,15 +345,21 @@ function extractLoaderOutputTypeName(sourceFile: SourceFile): string | null {
345
345
  const nonNull = ret.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
346
346
  if (nonNull.length === 1) ret = nonNull[0];
347
347
  }
348
- // Unwrap array element type — Product[] → "Product[]" (keyed as array)
348
+ // Unwrap array element type — Product[] → "Product[]" (keyed as array).
349
+ // When the element type has no resolvable name (primitives like `string[]`,
350
+ // or opaque types like `VNode[]`), there is NO meaningful output-type key.
351
+ // Returning the generic `Array` symbol here would bucket the loader under
352
+ // "Array", which then collides with EVERY array-typed section prop
353
+ // (`Collection[]`, `Tab[]`, …) and wrongly turns them into loader pickers.
349
354
  if (ret.isArray()) {
350
355
  const elType = ret.getArrayElementType();
351
- if (elType) {
352
- const elName = elType.getSymbol()?.getName() ?? elType.getAliasSymbol()?.getName() ?? null;
353
- if (elName) return `${elName}[]`;
354
- }
356
+ const elName = elType?.getSymbol()?.getName() ?? elType?.getAliasSymbol()?.getName() ?? null;
357
+ return elName ? `${elName}[]` : null;
355
358
  }
356
- return ret.getSymbol()?.getName() ?? ret.getAliasSymbol()?.getName() ?? null;
359
+ // Reject anonymous object returns (`{ }` → symbol name "__type"): they are
360
+ // not a nameable output type and would over-match anonymous-object props.
361
+ const name = ret.getSymbol()?.getName() ?? ret.getAliasSymbol()?.getName() ?? null;
362
+ return name && name !== "__type" ? name : null;
357
363
  }
358
364
 
359
365
  export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?: GenerationContext): any {
@@ -565,7 +571,12 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
565
571
  // baseHint strips "| null | undefined" so "ProductListingPage | null" → "ProductListingPage"
566
572
  if (ctx?.outputTypeToLoaderKeys) {
567
573
  const typeSym = propType.getSymbol() ?? propType.getAliasSymbol();
568
- const outputTypeName = typeSym?.getName() ?? baseHint;
574
+ // An array type's symbol name is always the generic "Array", which is
575
+ // never a meaningful loader output key — fall back to the AST type
576
+ // text (`baseHint`, e.g. "Collection[]") instead. The element-based
577
+ // `${elName}[]` lookup below handles arrays precisely.
578
+ const symName = typeSym?.getName();
579
+ const outputTypeName = symName && symName !== "Array" ? symName : baseHint;
569
580
  let matchingLoaders =
570
581
  ctx.outputTypeToLoaderKeys.get(outputTypeName) ??
571
582
  (outputTypeName !== baseHint ? ctx.outputTypeToLoaderKeys.get(baseHint) : undefined);