@decocms/blocks-cli 7.16.6 → 7.16.8
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.16.
|
|
3
|
+
"version": "7.16.8",
|
|
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.16.
|
|
33
|
+
"@decocms/blocks": "7.16.8",
|
|
34
34
|
"ts-morph": "^27.0.0",
|
|
35
35
|
"tsx": "^4.22.5"
|
|
36
36
|
},
|
|
@@ -100,6 +100,63 @@ describe("typeToJsonSchema with an unresolvable widget alias import", () => {
|
|
|
100
100
|
}, 30_000);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
describe("typeToJsonSchema with intersection types", () => {
|
|
104
|
+
it("keeps a branded primitive (`string & { __brand }`) as a string", () => {
|
|
105
|
+
const project = new Project({
|
|
106
|
+
useInMemoryFileSystem: true,
|
|
107
|
+
compilerOptions: { skipLibCheck: true },
|
|
108
|
+
});
|
|
109
|
+
const sf = project.createSourceFile(
|
|
110
|
+
"props.ts",
|
|
111
|
+
`
|
|
112
|
+
type Slug = string & { readonly __brand: unique symbol };
|
|
113
|
+
export interface Props { slug?: Slug }
|
|
114
|
+
`,
|
|
115
|
+
);
|
|
116
|
+
const schema = typeToJsonSchema(sf.getInterfaceOrThrow("Props").getType());
|
|
117
|
+
expect(schema.properties.slug.type).toBe("string");
|
|
118
|
+
}, 30_000);
|
|
119
|
+
|
|
120
|
+
it("expands an object intersection into a merged field set (recursive menu shape)", () => {
|
|
121
|
+
const project = new Project({
|
|
122
|
+
useInMemoryFileSystem: true,
|
|
123
|
+
compilerOptions: { skipLibCheck: true },
|
|
124
|
+
});
|
|
125
|
+
// Mirrors the granado header `SiteNavigationElement` recursive workaround:
|
|
126
|
+
// nested children written as `Leaf & { children?: Array<…> }`. Before the
|
|
127
|
+
// intersection branch these collapsed to `children: { items: { type: "string" } }`.
|
|
128
|
+
const sf = project.createSourceFile(
|
|
129
|
+
"props.ts",
|
|
130
|
+
`
|
|
131
|
+
interface Leaf {
|
|
132
|
+
/** The name of the item. */
|
|
133
|
+
name?: string;
|
|
134
|
+
/** URL of the item. */
|
|
135
|
+
url?: string;
|
|
136
|
+
}
|
|
137
|
+
export interface Props {
|
|
138
|
+
navItems?: Array<
|
|
139
|
+
Leaf & {
|
|
140
|
+
children?: Array<Leaf & { children?: Leaf[] }>;
|
|
141
|
+
}
|
|
142
|
+
>;
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
);
|
|
146
|
+
const schema = typeToJsonSchema(sf.getInterfaceOrThrow("Props").getType());
|
|
147
|
+
|
|
148
|
+
const item = schema.properties.navItems.items;
|
|
149
|
+
expect(item.type).toBe("object");
|
|
150
|
+
expect(Object.keys(item.properties).sort()).toEqual(["children", "name", "url"]);
|
|
151
|
+
|
|
152
|
+
const child = item.properties.children.items;
|
|
153
|
+
expect(child.type).toBe("object");
|
|
154
|
+
expect(child.properties.name.type).toBe("string");
|
|
155
|
+
expect(child.properties.url.type).toBe("string");
|
|
156
|
+
expect(child.properties.children.items.type).toBe("object");
|
|
157
|
+
}, 30_000);
|
|
158
|
+
});
|
|
159
|
+
|
|
103
160
|
// ---------------------------------------------------------------------------
|
|
104
161
|
// Default output path (.deco/) + legacy warning — subprocess, mirrors the
|
|
105
162
|
// pattern in generate-sections.test.ts. generate-schema.ts IS guarded by
|
|
@@ -430,6 +430,37 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
430
430
|
return result;
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
+
// Intersection (A & B). TypeScript merges the members, but ts-morph reports
|
|
434
|
+
// the type as neither object nor interface, so without this branch it falls
|
|
435
|
+
// through to the `{ type: "string" }` fallback — which is why recursive
|
|
436
|
+
// menu/navigation types written as `Leaf & { children?: Array<Leaf & …> }`
|
|
437
|
+
// lost every field and rendered as a bare string/block-ref in the CMS.
|
|
438
|
+
// Two shapes matter:
|
|
439
|
+
// - Branded primitives (`string & { __brand }`) → keep the primitive.
|
|
440
|
+
// - Object intersections → merge every member's properties into one object.
|
|
441
|
+
if (type.isIntersection()) {
|
|
442
|
+
const parts = type.getIntersectionTypes();
|
|
443
|
+
const primitive = parts.find((t) => t.isString() || t.isNumber() || t.isBoolean());
|
|
444
|
+
if (primitive) return typeToJsonSchema(primitive, new Set(visited), ctx);
|
|
445
|
+
|
|
446
|
+
const merged: any = { type: "object", properties: {} };
|
|
447
|
+
const required = new Set<string>();
|
|
448
|
+
for (const part of parts) {
|
|
449
|
+
const sub = typeToJsonSchema(part, new Set(visited), ctx);
|
|
450
|
+
if (sub?.type !== "object" || !sub.properties) continue;
|
|
451
|
+
Object.assign(merged.properties, sub.properties);
|
|
452
|
+
for (const r of sub.required ?? []) required.add(r);
|
|
453
|
+
// Carry object-level annotations (title, @titleBy, …) from members,
|
|
454
|
+
// first member wins so an earlier explicit value is never clobbered.
|
|
455
|
+
for (const [k, v] of Object.entries(sub)) {
|
|
456
|
+
if (k === "type" || k === "properties" || k === "required") continue;
|
|
457
|
+
if (!(k in merged)) merged[k] = v;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (required.size > 0) merged.required = [...required];
|
|
461
|
+
return merged;
|
|
462
|
+
}
|
|
463
|
+
|
|
433
464
|
if (type.isObject() || type.isInterface()) {
|
|
434
465
|
// Runtime-injected platform types (loader `url: URL`, `req: Request`, …)
|
|
435
466
|
// are not CMS-configurable — hide instead of expanding the whole DOM
|