From df6f8650e19178134d9a0dfddd31ef0654fb2c35 Mon Sep 17 00:00:00 2001 From: Cass Windred Date: Wed, 8 Oct 2025 18:23:40 +0100 Subject: [PATCH 1/2] Skip sections as with tabs --- packages/cli/src/commands/types/generate/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/types/generate/actions.ts b/packages/cli/src/commands/types/generate/actions.ts index 68c7f0046..eda1d428a 100644 --- a/packages/cli/src/commands/types/generate/actions.ts +++ b/packages/cli/src/commands/types/generate/actions.ts @@ -257,7 +257,7 @@ const getComponentPropertiesTypeAnnotations = async ( const acc = await accPromise; // Skip tabbed properties - if (key.startsWith("tab-")) { + if (key.startsWith("tab-") || value.type === "section") { return acc; } From b997d6c895e77f6beee0c63f9da5aaa62cff16f9 Mon Sep 17 00:00:00 2001 From: Markus Oberlehner Date: Mon, 17 Aug 2026 13:59:10 +0200 Subject: [PATCH 2/2] fix(cli): identify layout fields by type when generating types Tabs were skipped by their `tab-` key prefix. That prefix is only a naming convention of the editor, so the check both missed tabs keyed differently and dropped genuine content fields that happen to be named `tab-...`. Storyblok itself discriminates on the field type, so do the same and collect the layout-only types in one place. Checking the type also has to happen after the runtime type guard. Reading `value.type` before it crashed the whole command on a malformed schema entry, which the management API types as impossible but real spaces still produce. Fixes #349 --- .../commands/types/generate/actions.test.ts | 103 ++++++++++++++++++ .../src/commands/types/generate/actions.ts | 19 +++- packages/cli/src/types/schemas.ts | 2 + 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/types/generate/actions.test.ts b/packages/cli/src/commands/types/generate/actions.test.ts index 97f3fc940..87d51f040 100644 --- a/packages/cli/src/commands/types/generate/actions.test.ts +++ b/packages/cli/src/commands/types/generate/actions.test.ts @@ -1053,6 +1053,109 @@ describe("component property type annotations", () => { expect(result).not.toContain("tab-content"); }); + it("should skip group properties", async () => { + // The management API represents the editor's "Group" field as a `section`. It only + // arranges the fields listed in `keys`, so it never holds a value of its own. + const componentWithGroup: Component = { + name: "test_component", + display_name: "Test Component", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + id: 1, + schema: { + contact_details: { + type: "section", + keys: ["title"], + }, + title: { + type: "text", + required: true, + }, + }, + internal_tags_list: [], + internal_tag_ids: [], + }; + + const spaceData: SpaceComponentsData = { + components: [componentWithGroup], + datasources: [], + groups: [], + presets: [], + internalTags: [], + }; + + const result = await generateTypes(spaceData, { strict: false }); + + expect(result).toContain("title: string"); + expect(result).not.toContain("contact_details"); + }); + + it("should keep a regular field whose name starts with tab-", async () => { + // Tabs are identified by their type, not by their key. A content field that merely + // happens to be named `tab-...` still holds a value and belongs in the types. + const componentWithTabPrefixedField: Component = { + name: "test_component", + display_name: "Test Component", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + id: 1, + schema: { + "tab-title": { + type: "text", + required: true, + }, + }, + internal_tags_list: [], + internal_tag_ids: [], + }; + + const spaceData: SpaceComponentsData = { + components: [componentWithTabPrefixedField], + datasources: [], + groups: [], + presets: [], + internalTags: [], + }; + + const result = await generateTypes(spaceData, { strict: false }); + + expect(result).toContain('"tab-title": string'); + }); + + it("should skip malformed schema entries instead of failing", async () => { + // The management API types every schema entry as an object, but malformed spaces + // do occur. A single bad entry must not take down the whole command. + const componentWithMalformedEntry: Component = { + name: "test_component", + display_name: "Test Component", + created_at: "2023-01-01T00:00:00Z", + updated_at: "2023-01-01T00:00:00Z", + id: 1, + schema: { + broken: null as unknown as Component["schema"][string], + title: { + type: "text", + required: true, + }, + }, + internal_tags_list: [], + internal_tag_ids: [], + }; + + const spaceData: SpaceComponentsData = { + components: [componentWithMalformedEntry], + datasources: [], + groups: [], + presets: [], + internalTags: [], + }; + + const result = await generateTypes(spaceData, { strict: false }); + + expect(result).toContain("title: string"); + expect(result).not.toContain("broken"); + }); + it("should handle custom property type with customFieldsParser", async () => { // Create a component with custom property type const componentWithCustomType: Component = { diff --git a/packages/cli/src/commands/types/generate/actions.ts b/packages/cli/src/commands/types/generate/actions.ts index eda1d428a..3f5599480 100644 --- a/packages/cli/src/commands/types/generate/actions.ts +++ b/packages/cli/src/commands/types/generate/actions.ts @@ -16,7 +16,7 @@ import { join, resolve } from "pathe"; import { pathToFileURL } from "node:url"; import { resolvePath, saveToFile } from "../../../utils/filesystem"; import { readFileSync } from "node:fs"; -import type { ComponentPropertySchema } from "../../../types/schemas"; +import type { ComponentPropertySchema, ComponentPropertySchemaType } from "../../../types/schemas"; import { createComponentFile, createContentTypesFile, @@ -66,6 +66,11 @@ const SOURCE_MAP_START = "//# sourceMappingURL="; // Bundled declarations hoist every import to the top as a single line each. Binding // lists are normalized by the bundler, so a regex is sufficient to enumerate them. const IMPORT_STATEMENT = /^import\s+(?:(.+?)\s+from\s+)?["'][^"']+["'];?[ \t]*$/gm; +// Field types that exist purely to arrange other fields in the editor. They carry no +// value of their own, so a story never stores anything under their key. The management +// API calls the editor's "Group" field a `section`. +const LAYOUT_FIELD_TYPES = new Set(["section", "tab"]); +const isLayoutFieldType = (type: ComponentPropertySchemaType) => LAYOUT_FIELD_TYPES.has(type); const getDatasourceTypeTitle = (slug: string) => `${toPascalCase(slug)}DataSource`; const getImportedBindings = (content: string) => @@ -256,11 +261,6 @@ const getComponentPropertiesTypeAnnotations = async ( async (accPromise, [key, value]) => { const acc = await accPromise; - // Skip tabbed properties - if (key.startsWith("tab-") || value.type === "section") { - return acc; - } - // Type guard to ensure value is ComponentPropertySchema if (!value || typeof value !== "object" || !("type" in value)) { return acc; @@ -268,6 +268,13 @@ const getComponentPropertiesTypeAnnotations = async ( const schema = value as FieldSchema; const propertyType = schema.type; + + // Tabs and groups only arrange fields in the editor, so they never reach the + // story content and must not show up in the generated types. + if (isLayoutFieldType(propertyType)) { + return acc; + } + const propertyTypeAnnotation: JSONSchema = { [key]: getPropertyTypeAnnotation(schema, options.typePrefix, options.typeSuffix), }; diff --git a/packages/cli/src/types/schemas.ts b/packages/cli/src/types/schemas.ts index 733c89364..5b66b7f77 100644 --- a/packages/cli/src/types/schemas.ts +++ b/packages/cli/src/types/schemas.ts @@ -13,6 +13,8 @@ export type ComponentPropertySchemaType = | "number" | "option" | "options" + | "section" + | "tab" | "text" | "textarea";