@decocms/blocks-cli 7.16.8 → 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.
|
|
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.
|
|
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
|
|
@@ -27,6 +27,12 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
* --skip-apps Skip app schema generation
|
|
28
28
|
* --out Output file (default: ".deco/meta.gen.json")
|
|
29
29
|
* --platform Platform name (default: "cloudflare")
|
|
30
|
+
* --compose Run composeMeta() before writing so the output is
|
|
31
|
+
* SELF-CONTAINED (bakes in Page, matchers, __SECTION_REF__,
|
|
32
|
+
* Resolvable). For consumers that read meta.gen.json straight
|
|
33
|
+
* from disk with no runtime (FS-based Studio / Eitri stack).
|
|
34
|
+
* --framework With --compose, the value written to the `framework` field
|
|
35
|
+
* (default: "tanstack-start").
|
|
30
36
|
*
|
|
31
37
|
* If no `--out` is passed and the OLD default (src/server/admin/meta.gen.json)
|
|
32
38
|
* still exists on disk, a one-line legacy warning is printed to stderr and the
|
|
@@ -75,6 +81,15 @@ let APPS_REL = "src/apps";
|
|
|
75
81
|
let SKIP_APPS = false;
|
|
76
82
|
let OUT_REL = NEW_DEFAULT_OUT_REL;
|
|
77
83
|
let PLATFORM = "cloudflare";
|
|
84
|
+
// When true, run composeMeta() over the generated site meta before writing, so
|
|
85
|
+
// the output file is SELF-CONTAINED — it carries the framework block types
|
|
86
|
+
// (Page, matchers, __SECTION_REF__, Resolvable) that composeMeta otherwise
|
|
87
|
+
// injects at runtime. Required by consumers that read meta.gen.json straight
|
|
88
|
+
// from the filesystem with no runtime (e.g. the FS-based Studio / Eitri stack).
|
|
89
|
+
let COMPOSE = false;
|
|
90
|
+
// Value written to the composed meta's `framework` field (only used with
|
|
91
|
+
// --compose). Defaults to composeMeta's historical "tanstack-start".
|
|
92
|
+
let FRAMEWORK = "tanstack-start";
|
|
78
93
|
|
|
79
94
|
if (isMainModule()) {
|
|
80
95
|
SITE_NAMESPACE = arg("namespace", SITE_NAMESPACE);
|
|
@@ -87,6 +102,8 @@ if (isMainModule()) {
|
|
|
87
102
|
const outFileExplicit = argv.includes("--out");
|
|
88
103
|
OUT_REL = arg("out", NEW_DEFAULT_OUT_REL);
|
|
89
104
|
PLATFORM = arg("platform", PLATFORM);
|
|
105
|
+
COMPOSE = argv.includes("--compose");
|
|
106
|
+
FRAMEWORK = arg("framework", FRAMEWORK);
|
|
90
107
|
if (!outFileExplicit && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL))) {
|
|
91
108
|
warnLegacyArtifact(OLD_DEFAULT_OUT_REL, NEW_DEFAULT_OUT_REL);
|
|
92
109
|
}
|
|
@@ -328,15 +345,21 @@ function extractLoaderOutputTypeName(sourceFile: SourceFile): string | null {
|
|
|
328
345
|
const nonNull = ret.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
|
|
329
346
|
if (nonNull.length === 1) ret = nonNull[0];
|
|
330
347
|
}
|
|
331
|
-
// 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.
|
|
332
354
|
if (ret.isArray()) {
|
|
333
355
|
const elType = ret.getArrayElementType();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (elName) return `${elName}[]`;
|
|
337
|
-
}
|
|
356
|
+
const elName = elType?.getSymbol()?.getName() ?? elType?.getAliasSymbol()?.getName() ?? null;
|
|
357
|
+
return elName ? `${elName}[]` : null;
|
|
338
358
|
}
|
|
339
|
-
|
|
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;
|
|
340
363
|
}
|
|
341
364
|
|
|
342
365
|
export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?: GenerationContext): any {
|
|
@@ -548,7 +571,12 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
548
571
|
// baseHint strips "| null | undefined" so "ProductListingPage | null" → "ProductListingPage"
|
|
549
572
|
if (ctx?.outputTypeToLoaderKeys) {
|
|
550
573
|
const typeSym = propType.getSymbol() ?? propType.getAliasSymbol();
|
|
551
|
-
|
|
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;
|
|
552
580
|
let matchingLoaders =
|
|
553
581
|
ctx.outputTypeToLoaderKeys.get(outputTypeName) ??
|
|
554
582
|
(outputTypeName !== baseHint ? ctx.outputTypeToLoaderKeys.get(baseHint) : undefined);
|
|
@@ -1441,16 +1469,28 @@ function isMainModule(): boolean {
|
|
|
1441
1469
|
}
|
|
1442
1470
|
|
|
1443
1471
|
if (isMainModule()) {
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1472
|
+
// Wrapped in an async IIFE so --compose can dynamically import composeMeta
|
|
1473
|
+
// ONLY when requested — the default path (and any test importing this
|
|
1474
|
+
// module's pure exports) never pulls in the @decocms/blocks/cms barrel.
|
|
1475
|
+
void (async () => {
|
|
1476
|
+
let meta = generateMeta();
|
|
1477
|
+
if (COMPOSE) {
|
|
1478
|
+
const { composeMeta } = await import("@decocms/blocks/cms");
|
|
1479
|
+
// composeMeta returns @decocms/blocks' MetaResponse (platform optional);
|
|
1480
|
+
// this file's local MetaResponse requires platform. It's always present
|
|
1481
|
+
// (composeMeta spreads siteMeta, which set it), so the cast is safe.
|
|
1482
|
+
meta = composeMeta(meta, { framework: FRAMEWORK }) as MetaResponse;
|
|
1483
|
+
}
|
|
1484
|
+
const outPath = path.resolve(process.cwd(), OUT_REL);
|
|
1485
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1486
|
+
fs.writeFileSync(outPath, JSON.stringify(meta, null, 2));
|
|
1487
|
+
|
|
1488
|
+
const defCount = Object.keys(meta.schema.definitions).length;
|
|
1489
|
+
const secCount = Object.keys(meta.manifest.blocks.sections || {}).length;
|
|
1490
|
+
const ldrCount = Object.keys(meta.manifest.blocks.loaders || {}).length;
|
|
1491
|
+
const appCount = Object.keys(meta.manifest.blocks.apps || {}).length;
|
|
1492
|
+
console.log(
|
|
1493
|
+
`\nGenerated schema${COMPOSE ? " (self-contained)" : ""}: ${defCount} definitions, ${secCount} sections, ${ldrCount} loaders, ${appCount} apps → ${path.relative(process.cwd(), outPath)}`,
|
|
1494
|
+
);
|
|
1495
|
+
})();
|
|
1456
1496
|
}
|