diff --git a/packages/capi-client/src/generated/types/block.ts b/packages/capi-client/src/generated/types/block.ts index 698932b77..f361ba9cf 100644 --- a/packages/capi-client/src/generated/types/block.ts +++ b/packages/capi-client/src/generated/types/block.ts @@ -1,11 +1,8 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { - Component as ComponentGenerated, - Field, -} from './_sources'; -import type { Override } from './_utils'; +import type { Component as ComponentGenerated, Field } from "./_sources"; +import type { Override } from "./_utils"; /** * Ordered array of named fields — the content-shape DSL form `defineBlock` @@ -23,42 +20,46 @@ export type Block< TFields extends BlockFields = BlockFields, TIsRoot extends boolean = boolean, TIsNestable extends boolean = boolean, -> = Override, { - name: TName; - fields: TFields; - is_root?: TIsRoot; - is_nestable?: TIsNestable; - /** - * Escape hatch for pinning this block to a Storyblok UI-managed component - * group by UUID. Component groups are normally maintained in code via the - * schema directory layout; set this only if you intentionally manage groups - * in the Storyblok UI, and fill in the group UUID yourself. When set, - * `schema push` diffs it and sends it to the Management API; when omitted, - * the block's remote group is left untouched. - * - * @deprecated Prefer maintaining component groups in code through the - * directory layout. - */ - component_group_uuid?: string | null; - /** - * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = - * explicitly ungrouped (push clears the group). Absent = unmanaged (push - * leaves the remote group untouched). - */ - folder?: string | null; -}>; +> = Override< + Omit, + { + name: TName; + fields: TFields; + is_root?: TIsRoot; + is_nestable?: TIsNestable; + /** + * Escape hatch for pinning this block to a Storyblok UI-managed component + * group by UUID. Component groups are normally maintained in code via the + * schema directory layout; set this only if you intentionally manage groups + * in the Storyblok UI, and fill in the group UUID yourself. When set, + * `schema push` diffs it and sends it to the Management API; when omitted, + * the block's remote group is left untouched. + * + * @deprecated Prefer maintaining component groups in code through the + * directory layout. + */ + component_group_uuid?: string | null; + /** + * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = + * explicitly ungrouped (push clears the group). Absent = unmanaged (push + * leaves the remote group untouched). + */ + folder?: string | null; + } +>; /** * A root {@link Block} (`is_root: true`). Given a union of blocks, narrows to * its root members; with no argument it is the generic root-block type. */ -export type RootBlock = - Extract; +export type RootBlock = Extract; /** * A nestable {@link Block} (`is_nestable: true`). Given a union of blocks, * narrows to its nestable members; with no argument it is the generic * nestable-block type. */ -export type NestableBlock = - Extract; +export type NestableBlock = Extract< + T, + { is_nestable: true } +>; diff --git a/packages/capi-client/src/generated/types/field.ts b/packages/capi-client/src/generated/types/field.ts index 29182b09b..870837962 100644 --- a/packages/capi-client/src/generated/types/field.ts +++ b/packages/capi-client/src/generated/types/field.ts @@ -10,12 +10,18 @@ import type { PluginFieldValue, RichTextFieldValue, TableFieldValue, -} from './_sources'; -import type { Prettify } from './_utils'; -import type { Block, BlockFields } from './block'; +} from "./_sources"; +import type { Prettify } from "./_utils"; +import type { Block, BlockFields } from "./block"; export type { Field }; -export type { AssetFieldValue, MultilinkFieldValue, PluginFieldValue, RichTextFieldValue, TableFieldValue }; +export type { + AssetFieldValue, + MultilinkFieldValue, + PluginFieldValue, + RichTextFieldValue, + TableFieldValue, +}; /** * @deprecated Use {@link RichTextFieldValue} instead. Will be removed in a future major version. @@ -37,15 +43,45 @@ type IsBaseBlock = [Block] extends [T] ? true : false; * required (`required: true`) from optional fields. Each `F` is a member of the * field union, so it provably satisfies `FieldValue`'s `Field` constraint. */ -type ContentFields> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValue } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValue | null } +type ContentFields< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValue< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValue< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** Input (write) variant of {@link ContentFields}, resolving each field via {@link FieldValueInput}. */ -type ContentFieldsInput> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValueInput } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValueInput | null } +type ContentFieldsInput< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValueInput< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValueInput< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** @@ -54,27 +90,41 @@ type ContentFieldsInput> = +export type BlockContent< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid: string; component: TBlock['name']; _editable?: string } - & ContentFields - > + { _uid: string; component: TBlock["name"]; _editable?: string } & ContentFields< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; /** Input variant of {@link BlockContent} for write operations (creating/updating stories via the MAPI). `_uid` is optional. */ -export type BlockContentInput> = +export type BlockContentInput< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentInputBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid?: string; component: TBlock['name']; _editable?: string } - & ContentFieldsInput - > + { _uid?: string; component: TBlock["name"]; _editable?: string } & ContentFieldsInput< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; export type BlocksFieldValue< @@ -84,7 +134,7 @@ export type BlocksFieldValue< > = BlockContent[]; /** Union of all valid Storyblok field type discriminants (e.g., `text`, `bloks`). */ -export type FieldType = Field['type']; +export type FieldType = Field["type"]; interface FieldTypeValueMap { text: string; @@ -112,10 +162,11 @@ interface FieldTypeValueMap { custom: PluginFieldValue; } -type IsNestable = - T extends { is_nestable: false } ? false - : T extends { is_nestable: true } ? true - : true; +type IsNestable = T extends { is_nestable: false } + ? false + : T extends { is_nestable: true } + ? true + : true; type AllowEntry = string | { folder: string }; @@ -130,22 +181,31 @@ type AllowEntry = string | { folder: string }; * string path on both the block's `folder` and the field's `allow`: a shared ref * carries the exact path on both sides, so no drift is possible. */ -type MatchesFolder = - TBlock extends { folder: infer BF extends string } - ? Lowercase extends Lowercase | `${Lowercase}/${string}` ? TBlock : never - : never; +type MatchesFolder = TBlock extends { + folder: infer BF extends string; +} + ? Lowercase extends Lowercase | `${Lowercase}/${string}` + ? TBlock + : never + : never; -type ApplyAllow = TField extends { allow: ReadonlyArray } +type ApplyAllow = TField extends { + allow: ReadonlyArray; +} ? TAllowed extends string - // keep only the registry blocks named in `allow` - ? Extract + ? // keep only the registry blocks named in `allow` + Extract : TAllowed extends { folder: infer F extends string } - // keep registry blocks in the folder (or any nested folder) - ? TBlocks extends any ? MatchesFolder : never + ? // keep registry blocks in the folder (or any nested folder) + TBlocks extends any + ? MatchesFolder + : never + : never + : // no `allow`: distribute over the registry, keeping nestable blocks + TBlocks extends any + ? IsNestable extends true + ? TBlocks : never - // no `allow`: distribute over the registry, keeping nestable blocks - : TBlocks extends any - ? IsNestable extends true ? TBlocks : never : never; /** @@ -154,7 +214,9 @@ type ApplyAllow = TField extends { allow: ReadonlyArray = TField extends { deny: ReadonlyArray } +type ApplyDeny = TField extends { + deny: ReadonlyArray; +} ? Exclude : TBlocks; @@ -170,12 +232,11 @@ type ApplyRestrictions = ApplyDeny = - TField extends { field_type: infer F extends string } - ? F extends keyof TFieldPlugins - ? Prettify - : PluginFieldValue - : PluginFieldValue; +type ResolveCustom = TField extends { field_type: infer F extends string } + ? F extends keyof TFieldPlugins + ? Prettify + : PluginFieldValue + : PluginFieldValue; /** Resolves a field definition to its runtime content value type (read). */ export type FieldValue< @@ -183,17 +244,17 @@ export type FieldValue< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentBase[] - : [TBlocks] extends [Block] - ? BlockContent, TBlocks, TFieldPlugins>[] - : BlockContentBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentBase[] + : [TBlocks] extends [Block] + ? BlockContent, TBlocks, TFieldPlugins>[] + : BlockContentBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; /** Resolves a field definition to its input value type (write). */ @@ -202,15 +263,15 @@ export type FieldValueInput< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentInputBase[] - : [TBlocks] extends [Block] - ? BlockContentInput, TBlocks, TFieldPlugins>[] - : BlockContentInputBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentInputBase[] + : [TBlocks] extends [Block] + ? BlockContentInput, TBlocks, TFieldPlugins>[] + : BlockContentInputBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; diff --git a/packages/capi-client/src/generated/types/story.ts b/packages/capi-client/src/generated/types/story.ts index d32a8fa23..6021a2a00 100644 --- a/packages/capi-client/src/generated/types/story.ts +++ b/packages/capi-client/src/generated/types/story.ts @@ -1,10 +1,10 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { CapiStory as CapiStoryGenerated } from './_sources'; -import type { Override, Prettify } from './_utils'; -import type { Block, RootBlock } from './block'; -import type { BlockContent } from './field'; +import type { CapiStory as CapiStoryGenerated } from "./_sources"; +import type { Override, Prettify } from "./_utils"; +import type { Block, RootBlock } from "./block"; +import type { BlockContent } from "./field"; /** * Registry of all blocks, threaded through to resolve nested `bloks` fields. @@ -27,9 +27,13 @@ export type Story< // caller passed root block(s) directly → use them as the content type [TBlockOrBlocks] extends [RootBlock] ? CapiStoryWithSchemaContent - // caller passed the full block union → derive root blocks, thread the union as the registry - : [TBlocks] extends [NoBlocks] - ? CapiStoryWithSchemaContent, TBlockOrBlocks, TFieldPlugins> - // caller passed both → honour the explicit registry - : CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> + : // caller passed the full block union → derive root blocks, thread the union as the registry + [TBlocks] extends [NoBlocks] + ? CapiStoryWithSchemaContent< + Extract, + TBlockOrBlocks, + TFieldPlugins + > + : // caller passed both → honour the explicit registry + CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> >; diff --git a/packages/cli/src/generated/overlay/types.gen.ts b/packages/cli/src/generated/overlay/types.gen.ts index f46f57510..bccdd3783 100644 --- a/packages/cli/src/generated/overlay/types.gen.ts +++ b/packages/cli/src/generated/overlay/types.gen.ts @@ -9,16 +9,16 @@ export type RichTextMark = RichTextFieldValueRichTextMark; /** * Rich text field type - structured rich text document (ProseMirror/Tiptap format) */ -export interface RichTextFieldValueRoot { - /** - * Root node type — always "doc" - */ - type: 'doc'; - /** - * Top-level rich text nodes - */ - content: Array; -} +export type RichTextFieldValueRoot = { + /** + * Root node type — always "doc" + */ + type: 'doc'; + /** + * Top-level rich text nodes + */ + content: Array; +}; /** * A rich text document node @@ -30,389 +30,389 @@ export type RichTextFieldValueRichTextNode = RichTextFieldValueParagraphNode | R */ export type RichTextFieldValueRichTextMark = RichTextFieldValueLinkMark | RichTextFieldValueBoldMark | RichTextFieldValueItalicMark | RichTextFieldValueStrikeMark | RichTextFieldValueUnderlineMark | RichTextFieldValueCodeMark | RichTextFieldValueSuperscriptMark | RichTextFieldValueSubscriptMark | RichTextFieldValueHighlightMark | RichTextFieldValueTextStyleMark | RichTextFieldValueAnchorMark | RichTextFieldValueStyledMark; -export interface RichTextFieldValueParagraphNode { - type: 'paragraph'; - attrs?: { - /** - * Text alignment - */ - textAlign: 'left' | 'center' | 'right' | 'justify' | null; +export type RichTextFieldValueParagraphNode = { + type: 'paragraph'; + attrs?: { + /** + * Text alignment + */ + textAlign: 'left' | 'center' | 'right' | 'justify' | null; + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; + +export type RichTextFieldValueTextNode = { + type: 'text'; /** - * Text direction + * The text content */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} + text: string; + marks?: Array; +}; -export interface RichTextFieldValueTextNode { - type: 'text'; - /** - * The text content - */ - text: string; - marks?: Array; -} +export type RichTextFieldValueHeadingNode = { + type: 'heading'; + attrs: { + /** + * Heading level (h1–h6) + */ + level: 1 | 2 | 3 | 4 | 5 | 6 | null; + /** + * Text alignment + */ + textAlign: 'left' | 'center' | 'right' | 'justify' | null; + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueHeadingNode { - type: 'heading'; - attrs: { - /** - * Heading level (h1–h6) - */ - level: 1 | 2 | 3 | 4 | 5 | 6 | null; - /** - * Text alignment - */ - textAlign: 'left' | 'center' | 'right' | 'justify' | null; - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueBlockquoteNode = { + type: 'blockquote'; + attrs?: { + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueBlockquoteNode { - type: 'blockquote'; - attrs?: { - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueBulletListNode = { + type: 'bullet_list'; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueBulletListNode { - type: 'bullet_list'; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueOrderedListNode = { + type: 'ordered_list'; + attrs: { + /** + * Starting number for the ordered list + */ + order?: number; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueOrderedListNode { - type: 'ordered_list'; - attrs: { - /** - * Starting number for the ordered list - */ - order?: number; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueListItemNode = { + type: 'list_item'; + attrs?: { + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueListItemNode { - type: 'list_item'; - attrs?: { - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueCodeBlockNode = { + type: 'code_block'; + attrs: { + /** + * Language class (e.g. "language-typescript") + */ + class: string | null; + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueCodeBlockNode { - type: 'code_block'; - attrs: { - /** - * Language class (e.g. "language-typescript") - */ - class: string | null; - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueHardBreakNode = { + type: 'hard_break'; +}; -export interface RichTextFieldValueHardBreakNode { - type: 'hard_break'; -} +export type RichTextFieldValueHorizontalRuleNode = { + type: 'horizontal_rule'; +}; -export interface RichTextFieldValueHorizontalRuleNode { - type: 'horizontal_rule'; -} +export type RichTextFieldValueImageNode = { + type: 'image'; + attrs: { + /** + * Storyblok asset ID + */ + id: number | null; + /** + * Image URL + */ + src: string | null; + /** + * Alternative text + */ + alt: string | null; + title: string | null; + source: string | null; + copyright: string | null; + /** + * Asset metadata + */ + meta_data: { + alt?: string | null; + title?: string | null; + source?: string | null; + copyright?: string | null; + [key: string]: string | null | string | null | string | null | string | null | string | null | undefined; + } | null; + }; +}; -export interface RichTextFieldValueImageNode { - type: 'image'; - attrs: { - /** - * Storyblok asset ID - */ - id: number | null; - /** - * Image URL - */ - src: string | null; - /** - * Alternative text - */ - alt: string | null; - title: string | null; - source: string | null; - copyright: string | null; - /** - * Asset metadata - */ - meta_data: { - alt?: string | null; - title?: string | null; - source?: string | null; - copyright?: string | null; - [key: string]: string | null | string | null | string | null | string | null | string | null | undefined; - } | null; - }; -} +export type RichTextFieldValueEmojiNode = { + type: 'emoji'; + attrs: { + /** + * Emoji name/slug + */ + name: string; + /** + * Emoji character + */ + emoji: string; + /** + * URL to a fallback image for unsupported environments + */ + fallbackImage: string; + }; +}; -export interface RichTextFieldValueEmojiNode { - type: 'emoji'; - attrs: { - /** - * Emoji name/slug - */ - name: string; - /** - * Emoji character - */ - emoji: string; - /** - * URL to a fallback image for unsupported environments - */ - fallbackImage: string; - }; -} +export type RichTextFieldValueTableNode = { + type: 'table'; + content?: Array; +}; + +export type RichTextFieldValueTableRowNode = { + type: 'tableRow'; + content?: Array; +}; + +export type RichTextFieldValueTableCellNode = { + type: 'tableCell'; + attrs: { + colspan?: number; + rowspan?: number; + /** + * Column widths in pixels + */ + colwidth?: Array | null; + backgroundColor?: string | null; + }; + content?: Array; +}; + +export type RichTextFieldValueTableHeaderNode = { + type: 'tableHeader'; + attrs: { + colspan?: number; + rowspan?: number; + /** + * Column widths in pixels + */ + colwidth?: Array | null; + }; + content?: Array; +}; + +export type RichTextFieldValueBlockNode = { + type: 'blok'; + attrs: { + /** + * Block instance ID + */ + id: string | null; + /** + * Array of embedded component instances + */ + body: Array | null; + }; +}; -export interface RichTextFieldValueTableNode { - type: 'table'; - content?: Array; -} +export type RichTextFieldValueLinkMark = { + type: 'link'; + /** + * Link attributes + */ + attrs: { + /** + * Link URL + */ + href: string | null; + /** + * UUID of the linked story (for internal links) + */ + uuid: string | null; + /** + * Anchor/fragment identifier + */ + anchor: string | null; + /** + * Link target attribute + */ + target: '_self' | '_blank' | '_parent' | '_top' | null; + /** + * Type of link + */ + linktype: 'story' | 'url' | 'email' | 'asset' | null; + /** + * Custom link attributes + */ + custom?: { + [key: string]: unknown; + } | null; + }; +}; + +export type RichTextFieldValueBoldMark = { + type: 'bold'; +}; + +export type RichTextFieldValueItalicMark = { + type: 'italic'; +}; + +export type RichTextFieldValueStrikeMark = { + type: 'strike'; +}; + +export type RichTextFieldValueUnderlineMark = { + type: 'underline'; +}; + +export type RichTextFieldValueCodeMark = { + type: 'code'; +}; + +export type RichTextFieldValueSuperscriptMark = { + type: 'superscript'; +}; + +export type RichTextFieldValueSubscriptMark = { + type: 'subscript'; +}; -export interface RichTextFieldValueTableRowNode { - type: 'tableRow'; - content?: Array; -} +export type RichTextFieldValueHighlightMark = { + type: 'highlight'; + attrs: { + /** + * Highlight color (CSS color value) + */ + color: string | null; + }; +}; + +export type RichTextFieldValueTextStyleMark = { + type: 'textStyle'; + attrs: { + color?: string | null; + id?: string | null; + class?: string | null; + }; +}; + +export type RichTextFieldValueAnchorMark = { + type: 'anchor'; + attrs: { + /** + * Anchor identifier + */ + id: string; + }; +}; -export interface RichTextFieldValueTableCellNode { - type: 'tableCell'; - attrs: { - colspan?: number; - rowspan?: number; +export type RichTextFieldValueStyledMark = { + type: 'styled'; + attrs: { + /** + * CSS class name + */ + class: string | null; + }; +}; + +/** + * Content object for creating or updating a component instance. Contains a component technical name and dynamic fields whose values depend on the component's schema field types. The _uid is optional — Storyblok will auto-generate one if not provided. + */ +export type BlockContentInputRoot = { /** - * Column widths in pixels + * Unique identifier for this component instance (auto-generated when omitted) */ - colwidth?: Array | null; - backgroundColor?: string | null; - }; - content?: Array; -} - -export interface RichTextFieldValueTableHeaderNode { - type: 'tableHeader'; - attrs: { - colspan?: number; - rowspan?: number; + _uid?: string; /** - * Column widths in pixels + * Technical name of the component (content type or nestable block) */ - colwidth?: Array | null; - }; - content?: Array; -} + component: string; + /** + * Storyblok editor markup string for inline editing (present in draft/preview mode) + */ + _editable?: string; + [key: string]: null | string | number | boolean | Array | AssetFieldValueRoot | MultilinkFieldValueRoot | TableFieldValueRoot | RichTextFieldValueRoot | PluginFieldValueRoot | string | undefined; +}; -export interface RichTextFieldValueBlockNode { - type: 'blok'; - attrs: { +/** + * Asset field type - single asset (image, video, audio, or document) + */ +export type AssetFieldValueRoot = { /** - * Block instance ID + * Identifies this as an asset field */ - id: string | null; + fieldtype: 'asset'; /** - * Array of embedded component instances + * Numeric ID of the asset */ - body: Array | null; - }; -} - -export interface RichTextFieldValueLinkMark { - type: 'link'; - /** - * Link attributes - */ - attrs: { + id: number | null; /** - * Link URL + * Alternative text for the asset */ - href: string | null; + alt: string | null; /** - * UUID of the linked story (for internal links) + * Name of the asset */ - uuid: string | null; + name?: string; /** - * Anchor/fragment identifier + * Focal point coordinates for cropping (format "123x456:789x1011") */ - anchor: string | null; + focus?: string | null; /** - * Link target attribute + * Title of the asset */ - target: '_self' | '_blank' | '_parent' | '_top' | null; + title?: string | null; /** - * Type of link + * Source attribution for the asset */ - linktype: 'story' | 'url' | 'email' | 'asset' | null; + source?: string | null; /** - * Custom link attributes + * URL to the asset file */ - custom?: { - [key: string]: unknown; - } | null; - }; -} - -export interface RichTextFieldValueBoldMark { - type: 'bold'; -} - -export interface RichTextFieldValueItalicMark { - type: 'italic'; -} - -export interface RichTextFieldValueStrikeMark { - type: 'strike'; -} - -export interface RichTextFieldValueUnderlineMark { - type: 'underline'; -} - -export interface RichTextFieldValueCodeMark { - type: 'code'; -} - -export interface RichTextFieldValueSuperscriptMark { - type: 'superscript'; -} - -export interface RichTextFieldValueSubscriptMark { - type: 'subscript'; -} - -export interface RichTextFieldValueHighlightMark { - type: 'highlight'; - attrs: { + filename: string; /** - * Highlight color (CSS color value) + * Copyright information */ - color: string | null; - }; -} - -export interface RichTextFieldValueTextStyleMark { - type: 'textStyle'; - attrs: { - color?: string | null; - id?: string | null; - class?: string | null; - }; -} - -export interface RichTextFieldValueAnchorMark { - type: 'anchor'; - attrs: { + copyright?: string | null; /** - * Anchor identifier + * Additional metadata for the asset */ - id: string; - }; -} - -export interface RichTextFieldValueStyledMark { - type: 'styled'; - attrs: { + meta_data?: { + [key: string]: unknown; + }; /** - * CSS class name + * Whether the asset is hosted externally */ - class: string | null; - }; -} - -/** - * Content object for creating or updating a component instance. Contains a component technical name and dynamic fields whose values depend on the component's schema field types. The _uid is optional — Storyblok will auto-generate one if not provided. - */ -export interface BlockContentInputRoot { - /** - * Unique identifier for this component instance (auto-generated when omitted) - */ - _uid?: string; - /** - * Technical name of the component (content type or nestable block) - */ - component: string; - /** - * Storyblok editor markup string for inline editing (present in draft/preview mode) - */ - _editable?: string; - [key: string]: null | string | number | boolean | Array | AssetFieldValueRoot | MultilinkFieldValueRoot | TableFieldValueRoot | RichTextFieldValueRoot | PluginFieldValueRoot | string | undefined; -} - -/** - * Asset field type - single asset (image, video, audio, or document) - */ -export interface AssetFieldValueRoot { - /** - * Identifies this as an asset field - */ - fieldtype: 'asset'; - /** - * Numeric ID of the asset - */ - id: number | null; - /** - * Alternative text for the asset - */ - alt: string | null; - /** - * Name of the asset - */ - name?: string; - /** - * Focal point coordinates for cropping (format "123x456:789x1011") - */ - focus?: string | null; - /** - * Title of the asset - */ - title?: string | null; - /** - * Source attribution for the asset - */ - source?: string | null; - /** - * URL to the asset file - */ - filename: string; - /** - * Copyright information - */ - copyright?: string | null; - /** - * Additional metadata for the asset - */ - meta_data?: { - [key: string]: unknown; - }; - /** - * Whether the asset is hosted externally - */ - is_external_url?: boolean; -} + is_external_url?: boolean; +}; /** * Multilink field type - link to internal stories, external URLs, emails, or assets. @@ -422,132 +422,132 @@ export type MultilinkFieldValueRoot = MultilinkFieldValueStoryLink | MultilinkFi /** * Table field type - structured table data */ -export interface TableFieldValueRoot { - /** - * Table header cells - */ - thead: Array<{ - _uid?: string; - component?: '_table_head'; +export type TableFieldValueRoot = { /** - * Header cell content + * Table header cells */ - value?: string; - }>; - /** - * Table body rows - */ - tbody: Array<{ - _uid?: string; - component?: '_table_row'; + thead: Array<{ + _uid?: string; + component?: '_table_head'; + /** + * Header cell content + */ + value?: string; + }>; /** - * Cells in this row - */ - body?: Array<{ - _uid?: string; - component?: '_table_col'; - /** - * Cell content - */ - value?: string; + * Table body rows + */ + tbody: Array<{ + _uid?: string; + component?: '_table_row'; + /** + * Cells in this row + */ + body?: Array<{ + _uid?: string; + component?: '_table_col'; + /** + * Cell content + */ + value?: string; + }>; }>; - }>; -} +}; /** * Plugin/Custom field type - field plugin with custom structure */ -export interface PluginFieldValueRoot { - /** - * Technical name of the field plugin - */ - plugin: string; - /** - * Unique identifier for this plugin field instance - */ - _uid?: string; - [key: string]: unknown | string | undefined; -} +export type PluginFieldValueRoot = { + /** + * Technical name of the field plugin + */ + plugin: string; + /** + * Unique identifier for this plugin field instance + */ + _uid?: string; + [key: string]: unknown | string | undefined; +}; /** * Link to an internal Storyblok story. */ export type MultilinkFieldValueStoryLink = MultilinkFieldValueSharedLink & { - linktype: 'story'; - /** - * Anchor/fragment identifier for the story link - */ - anchor?: string | null; - /** - * Link relationship attribute - */ - rel?: string; - /** - * Link title attribute - */ - title?: string; + linktype: 'story'; + /** + * Anchor/fragment identifier for the story link + */ + anchor?: string | null; + /** + * Link relationship attribute + */ + rel?: string; + /** + * Link title attribute + */ + title?: string; } & { - [key: string]: string | null; + [key: string]: string | null; }; /** * Link to an external URL. */ export type MultilinkFieldValueUrlLink = MultilinkFieldValueSharedLink & { - linktype: 'url'; - /** - * Link relationship attribute - */ - rel?: string; - /** - * Link title attribute - */ - title?: string; + linktype: 'url'; + /** + * Link relationship attribute + */ + rel?: string; + /** + * Link title attribute + */ + title?: string; } & { - [key: string]: string; + [key: string]: string; }; /** * Link to an email address. */ export type MultilinkFieldValueEmailLink = MultilinkFieldValueSharedLink & { - linktype: 'email'; - /** - * Email address - */ - email?: string; + linktype: 'email'; + /** + * Email address + */ + email?: string; }; /** * Link to a Storyblok asset. */ export type MultilinkFieldValueAssetLink = MultilinkFieldValueSharedLink & { - linktype: 'asset'; -}; - -export interface MultilinkFieldValueSharedLink { - /** - * Identifies this as a multilink field - */ - fieldtype: 'multilink'; - /** - * Type of link - */ - linktype: string; - /** - * UUID of the linked story for internal links - */ - id: string; - /** - * URL for external links - */ - url: string; - /** - * Cached URL path for the linked story - */ - cached_url: string; - /** - * Link target attribute - */ - target?: '_self' | '_blank'; -} + linktype: 'asset'; +}; + +export type MultilinkFieldValueSharedLink = { + /** + * Identifies this as a multilink field + */ + fieldtype: 'multilink'; + /** + * Type of link + */ + linktype: string; + /** + * UUID of the linked story for internal links + */ + id: string; + /** + * URL for external links + */ + url: string; + /** + * Cached URL path for the linked story + */ + cached_url: string; + /** + * Link target attribute + */ + target?: '_self' | '_blank'; +}; diff --git a/packages/live-preview/src/generated/types/block.ts b/packages/live-preview/src/generated/types/block.ts index 698932b77..f361ba9cf 100644 --- a/packages/live-preview/src/generated/types/block.ts +++ b/packages/live-preview/src/generated/types/block.ts @@ -1,11 +1,8 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { - Component as ComponentGenerated, - Field, -} from './_sources'; -import type { Override } from './_utils'; +import type { Component as ComponentGenerated, Field } from "./_sources"; +import type { Override } from "./_utils"; /** * Ordered array of named fields — the content-shape DSL form `defineBlock` @@ -23,42 +20,46 @@ export type Block< TFields extends BlockFields = BlockFields, TIsRoot extends boolean = boolean, TIsNestable extends boolean = boolean, -> = Override, { - name: TName; - fields: TFields; - is_root?: TIsRoot; - is_nestable?: TIsNestable; - /** - * Escape hatch for pinning this block to a Storyblok UI-managed component - * group by UUID. Component groups are normally maintained in code via the - * schema directory layout; set this only if you intentionally manage groups - * in the Storyblok UI, and fill in the group UUID yourself. When set, - * `schema push` diffs it and sends it to the Management API; when omitted, - * the block's remote group is left untouched. - * - * @deprecated Prefer maintaining component groups in code through the - * directory layout. - */ - component_group_uuid?: string | null; - /** - * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = - * explicitly ungrouped (push clears the group). Absent = unmanaged (push - * leaves the remote group untouched). - */ - folder?: string | null; -}>; +> = Override< + Omit, + { + name: TName; + fields: TFields; + is_root?: TIsRoot; + is_nestable?: TIsNestable; + /** + * Escape hatch for pinning this block to a Storyblok UI-managed component + * group by UUID. Component groups are normally maintained in code via the + * schema directory layout; set this only if you intentionally manage groups + * in the Storyblok UI, and fill in the group UUID yourself. When set, + * `schema push` diffs it and sends it to the Management API; when omitted, + * the block's remote group is left untouched. + * + * @deprecated Prefer maintaining component groups in code through the + * directory layout. + */ + component_group_uuid?: string | null; + /** + * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = + * explicitly ungrouped (push clears the group). Absent = unmanaged (push + * leaves the remote group untouched). + */ + folder?: string | null; + } +>; /** * A root {@link Block} (`is_root: true`). Given a union of blocks, narrows to * its root members; with no argument it is the generic root-block type. */ -export type RootBlock = - Extract; +export type RootBlock = Extract; /** * A nestable {@link Block} (`is_nestable: true`). Given a union of blocks, * narrows to its nestable members; with no argument it is the generic * nestable-block type. */ -export type NestableBlock = - Extract; +export type NestableBlock = Extract< + T, + { is_nestable: true } +>; diff --git a/packages/live-preview/src/generated/types/field.ts b/packages/live-preview/src/generated/types/field.ts index 29182b09b..870837962 100644 --- a/packages/live-preview/src/generated/types/field.ts +++ b/packages/live-preview/src/generated/types/field.ts @@ -10,12 +10,18 @@ import type { PluginFieldValue, RichTextFieldValue, TableFieldValue, -} from './_sources'; -import type { Prettify } from './_utils'; -import type { Block, BlockFields } from './block'; +} from "./_sources"; +import type { Prettify } from "./_utils"; +import type { Block, BlockFields } from "./block"; export type { Field }; -export type { AssetFieldValue, MultilinkFieldValue, PluginFieldValue, RichTextFieldValue, TableFieldValue }; +export type { + AssetFieldValue, + MultilinkFieldValue, + PluginFieldValue, + RichTextFieldValue, + TableFieldValue, +}; /** * @deprecated Use {@link RichTextFieldValue} instead. Will be removed in a future major version. @@ -37,15 +43,45 @@ type IsBaseBlock = [Block] extends [T] ? true : false; * required (`required: true`) from optional fields. Each `F` is a member of the * field union, so it provably satisfies `FieldValue`'s `Field` constraint. */ -type ContentFields> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValue } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValue | null } +type ContentFields< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValue< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValue< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** Input (write) variant of {@link ContentFields}, resolving each field via {@link FieldValueInput}. */ -type ContentFieldsInput> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValueInput } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValueInput | null } +type ContentFieldsInput< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValueInput< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValueInput< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** @@ -54,27 +90,41 @@ type ContentFieldsInput> = +export type BlockContent< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid: string; component: TBlock['name']; _editable?: string } - & ContentFields - > + { _uid: string; component: TBlock["name"]; _editable?: string } & ContentFields< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; /** Input variant of {@link BlockContent} for write operations (creating/updating stories via the MAPI). `_uid` is optional. */ -export type BlockContentInput> = +export type BlockContentInput< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentInputBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid?: string; component: TBlock['name']; _editable?: string } - & ContentFieldsInput - > + { _uid?: string; component: TBlock["name"]; _editable?: string } & ContentFieldsInput< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; export type BlocksFieldValue< @@ -84,7 +134,7 @@ export type BlocksFieldValue< > = BlockContent[]; /** Union of all valid Storyblok field type discriminants (e.g., `text`, `bloks`). */ -export type FieldType = Field['type']; +export type FieldType = Field["type"]; interface FieldTypeValueMap { text: string; @@ -112,10 +162,11 @@ interface FieldTypeValueMap { custom: PluginFieldValue; } -type IsNestable = - T extends { is_nestable: false } ? false - : T extends { is_nestable: true } ? true - : true; +type IsNestable = T extends { is_nestable: false } + ? false + : T extends { is_nestable: true } + ? true + : true; type AllowEntry = string | { folder: string }; @@ -130,22 +181,31 @@ type AllowEntry = string | { folder: string }; * string path on both the block's `folder` and the field's `allow`: a shared ref * carries the exact path on both sides, so no drift is possible. */ -type MatchesFolder = - TBlock extends { folder: infer BF extends string } - ? Lowercase extends Lowercase | `${Lowercase}/${string}` ? TBlock : never - : never; +type MatchesFolder = TBlock extends { + folder: infer BF extends string; +} + ? Lowercase extends Lowercase | `${Lowercase}/${string}` + ? TBlock + : never + : never; -type ApplyAllow = TField extends { allow: ReadonlyArray } +type ApplyAllow = TField extends { + allow: ReadonlyArray; +} ? TAllowed extends string - // keep only the registry blocks named in `allow` - ? Extract + ? // keep only the registry blocks named in `allow` + Extract : TAllowed extends { folder: infer F extends string } - // keep registry blocks in the folder (or any nested folder) - ? TBlocks extends any ? MatchesFolder : never + ? // keep registry blocks in the folder (or any nested folder) + TBlocks extends any + ? MatchesFolder + : never + : never + : // no `allow`: distribute over the registry, keeping nestable blocks + TBlocks extends any + ? IsNestable extends true + ? TBlocks : never - // no `allow`: distribute over the registry, keeping nestable blocks - : TBlocks extends any - ? IsNestable extends true ? TBlocks : never : never; /** @@ -154,7 +214,9 @@ type ApplyAllow = TField extends { allow: ReadonlyArray = TField extends { deny: ReadonlyArray } +type ApplyDeny = TField extends { + deny: ReadonlyArray; +} ? Exclude : TBlocks; @@ -170,12 +232,11 @@ type ApplyRestrictions = ApplyDeny = - TField extends { field_type: infer F extends string } - ? F extends keyof TFieldPlugins - ? Prettify - : PluginFieldValue - : PluginFieldValue; +type ResolveCustom = TField extends { field_type: infer F extends string } + ? F extends keyof TFieldPlugins + ? Prettify + : PluginFieldValue + : PluginFieldValue; /** Resolves a field definition to its runtime content value type (read). */ export type FieldValue< @@ -183,17 +244,17 @@ export type FieldValue< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentBase[] - : [TBlocks] extends [Block] - ? BlockContent, TBlocks, TFieldPlugins>[] - : BlockContentBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentBase[] + : [TBlocks] extends [Block] + ? BlockContent, TBlocks, TFieldPlugins>[] + : BlockContentBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; /** Resolves a field definition to its input value type (write). */ @@ -202,15 +263,15 @@ export type FieldValueInput< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentInputBase[] - : [TBlocks] extends [Block] - ? BlockContentInput, TBlocks, TFieldPlugins>[] - : BlockContentInputBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentInputBase[] + : [TBlocks] extends [Block] + ? BlockContentInput, TBlocks, TFieldPlugins>[] + : BlockContentInputBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; diff --git a/packages/live-preview/src/generated/types/story.ts b/packages/live-preview/src/generated/types/story.ts index d32a8fa23..6021a2a00 100644 --- a/packages/live-preview/src/generated/types/story.ts +++ b/packages/live-preview/src/generated/types/story.ts @@ -1,10 +1,10 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { CapiStory as CapiStoryGenerated } from './_sources'; -import type { Override, Prettify } from './_utils'; -import type { Block, RootBlock } from './block'; -import type { BlockContent } from './field'; +import type { CapiStory as CapiStoryGenerated } from "./_sources"; +import type { Override, Prettify } from "./_utils"; +import type { Block, RootBlock } from "./block"; +import type { BlockContent } from "./field"; /** * Registry of all blocks, threaded through to resolve nested `bloks` fields. @@ -27,9 +27,13 @@ export type Story< // caller passed root block(s) directly → use them as the content type [TBlockOrBlocks] extends [RootBlock] ? CapiStoryWithSchemaContent - // caller passed the full block union → derive root blocks, thread the union as the registry - : [TBlocks] extends [NoBlocks] - ? CapiStoryWithSchemaContent, TBlockOrBlocks, TFieldPlugins> - // caller passed both → honour the explicit registry - : CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> + : // caller passed the full block union → derive root blocks, thread the union as the registry + [TBlocks] extends [NoBlocks] + ? CapiStoryWithSchemaContent< + Extract, + TBlockOrBlocks, + TFieldPlugins + > + : // caller passed both → honour the explicit registry + CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> >; diff --git a/packages/mapi-client/src/generated/types/block.ts b/packages/mapi-client/src/generated/types/block.ts index 698932b77..f361ba9cf 100644 --- a/packages/mapi-client/src/generated/types/block.ts +++ b/packages/mapi-client/src/generated/types/block.ts @@ -1,11 +1,8 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { - Component as ComponentGenerated, - Field, -} from './_sources'; -import type { Override } from './_utils'; +import type { Component as ComponentGenerated, Field } from "./_sources"; +import type { Override } from "./_utils"; /** * Ordered array of named fields — the content-shape DSL form `defineBlock` @@ -23,42 +20,46 @@ export type Block< TFields extends BlockFields = BlockFields, TIsRoot extends boolean = boolean, TIsNestable extends boolean = boolean, -> = Override, { - name: TName; - fields: TFields; - is_root?: TIsRoot; - is_nestable?: TIsNestable; - /** - * Escape hatch for pinning this block to a Storyblok UI-managed component - * group by UUID. Component groups are normally maintained in code via the - * schema directory layout; set this only if you intentionally manage groups - * in the Storyblok UI, and fill in the group UUID yourself. When set, - * `schema push` diffs it and sends it to the Management API; when omitted, - * the block's remote group is left untouched. - * - * @deprecated Prefer maintaining component groups in code through the - * directory layout. - */ - component_group_uuid?: string | null; - /** - * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = - * explicitly ungrouped (push clears the group). Absent = unmanaged (push - * leaves the remote group untouched). - */ - folder?: string | null; -}>; +> = Override< + Omit, + { + name: TName; + fields: TFields; + is_root?: TIsRoot; + is_nestable?: TIsNestable; + /** + * Escape hatch for pinning this block to a Storyblok UI-managed component + * group by UUID. Component groups are normally maintained in code via the + * schema directory layout; set this only if you intentionally manage groups + * in the Storyblok UI, and fill in the group UUID yourself. When set, + * `schema push` diffs it and sends it to the Management API; when omitted, + * the block's remote group is left untouched. + * + * @deprecated Prefer maintaining component groups in code through the + * directory layout. + */ + component_group_uuid?: string | null; + /** + * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = + * explicitly ungrouped (push clears the group). Absent = unmanaged (push + * leaves the remote group untouched). + */ + folder?: string | null; + } +>; /** * A root {@link Block} (`is_root: true`). Given a union of blocks, narrows to * its root members; with no argument it is the generic root-block type. */ -export type RootBlock = - Extract; +export type RootBlock = Extract; /** * A nestable {@link Block} (`is_nestable: true`). Given a union of blocks, * narrows to its nestable members; with no argument it is the generic * nestable-block type. */ -export type NestableBlock = - Extract; +export type NestableBlock = Extract< + T, + { is_nestable: true } +>; diff --git a/packages/mapi-client/src/generated/types/field.ts b/packages/mapi-client/src/generated/types/field.ts index 29182b09b..870837962 100644 --- a/packages/mapi-client/src/generated/types/field.ts +++ b/packages/mapi-client/src/generated/types/field.ts @@ -10,12 +10,18 @@ import type { PluginFieldValue, RichTextFieldValue, TableFieldValue, -} from './_sources'; -import type { Prettify } from './_utils'; -import type { Block, BlockFields } from './block'; +} from "./_sources"; +import type { Prettify } from "./_utils"; +import type { Block, BlockFields } from "./block"; export type { Field }; -export type { AssetFieldValue, MultilinkFieldValue, PluginFieldValue, RichTextFieldValue, TableFieldValue }; +export type { + AssetFieldValue, + MultilinkFieldValue, + PluginFieldValue, + RichTextFieldValue, + TableFieldValue, +}; /** * @deprecated Use {@link RichTextFieldValue} instead. Will be removed in a future major version. @@ -37,15 +43,45 @@ type IsBaseBlock = [Block] extends [T] ? true : false; * required (`required: true`) from optional fields. Each `F` is a member of the * field union, so it provably satisfies `FieldValue`'s `Field` constraint. */ -type ContentFields> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValue } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValue | null } +type ContentFields< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValue< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValue< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** Input (write) variant of {@link ContentFields}, resolving each field via {@link FieldValueInput}. */ -type ContentFieldsInput> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValueInput } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValueInput | null } +type ContentFieldsInput< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValueInput< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValueInput< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** @@ -54,27 +90,41 @@ type ContentFieldsInput> = +export type BlockContent< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid: string; component: TBlock['name']; _editable?: string } - & ContentFields - > + { _uid: string; component: TBlock["name"]; _editable?: string } & ContentFields< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; /** Input variant of {@link BlockContent} for write operations (creating/updating stories via the MAPI). `_uid` is optional. */ -export type BlockContentInput> = +export type BlockContentInput< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentInputBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid?: string; component: TBlock['name']; _editable?: string } - & ContentFieldsInput - > + { _uid?: string; component: TBlock["name"]; _editable?: string } & ContentFieldsInput< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; export type BlocksFieldValue< @@ -84,7 +134,7 @@ export type BlocksFieldValue< > = BlockContent[]; /** Union of all valid Storyblok field type discriminants (e.g., `text`, `bloks`). */ -export type FieldType = Field['type']; +export type FieldType = Field["type"]; interface FieldTypeValueMap { text: string; @@ -112,10 +162,11 @@ interface FieldTypeValueMap { custom: PluginFieldValue; } -type IsNestable = - T extends { is_nestable: false } ? false - : T extends { is_nestable: true } ? true - : true; +type IsNestable = T extends { is_nestable: false } + ? false + : T extends { is_nestable: true } + ? true + : true; type AllowEntry = string | { folder: string }; @@ -130,22 +181,31 @@ type AllowEntry = string | { folder: string }; * string path on both the block's `folder` and the field's `allow`: a shared ref * carries the exact path on both sides, so no drift is possible. */ -type MatchesFolder = - TBlock extends { folder: infer BF extends string } - ? Lowercase extends Lowercase | `${Lowercase}/${string}` ? TBlock : never - : never; +type MatchesFolder = TBlock extends { + folder: infer BF extends string; +} + ? Lowercase extends Lowercase | `${Lowercase}/${string}` + ? TBlock + : never + : never; -type ApplyAllow = TField extends { allow: ReadonlyArray } +type ApplyAllow = TField extends { + allow: ReadonlyArray; +} ? TAllowed extends string - // keep only the registry blocks named in `allow` - ? Extract + ? // keep only the registry blocks named in `allow` + Extract : TAllowed extends { folder: infer F extends string } - // keep registry blocks in the folder (or any nested folder) - ? TBlocks extends any ? MatchesFolder : never + ? // keep registry blocks in the folder (or any nested folder) + TBlocks extends any + ? MatchesFolder + : never + : never + : // no `allow`: distribute over the registry, keeping nestable blocks + TBlocks extends any + ? IsNestable extends true + ? TBlocks : never - // no `allow`: distribute over the registry, keeping nestable blocks - : TBlocks extends any - ? IsNestable extends true ? TBlocks : never : never; /** @@ -154,7 +214,9 @@ type ApplyAllow = TField extends { allow: ReadonlyArray = TField extends { deny: ReadonlyArray } +type ApplyDeny = TField extends { + deny: ReadonlyArray; +} ? Exclude : TBlocks; @@ -170,12 +232,11 @@ type ApplyRestrictions = ApplyDeny = - TField extends { field_type: infer F extends string } - ? F extends keyof TFieldPlugins - ? Prettify - : PluginFieldValue - : PluginFieldValue; +type ResolveCustom = TField extends { field_type: infer F extends string } + ? F extends keyof TFieldPlugins + ? Prettify + : PluginFieldValue + : PluginFieldValue; /** Resolves a field definition to its runtime content value type (read). */ export type FieldValue< @@ -183,17 +244,17 @@ export type FieldValue< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentBase[] - : [TBlocks] extends [Block] - ? BlockContent, TBlocks, TFieldPlugins>[] - : BlockContentBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentBase[] + : [TBlocks] extends [Block] + ? BlockContent, TBlocks, TFieldPlugins>[] + : BlockContentBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; /** Resolves a field definition to its input value type (write). */ @@ -202,15 +263,15 @@ export type FieldValueInput< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentInputBase[] - : [TBlocks] extends [Block] - ? BlockContentInput, TBlocks, TFieldPlugins>[] - : BlockContentInputBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentInputBase[] + : [TBlocks] extends [Block] + ? BlockContentInput, TBlocks, TFieldPlugins>[] + : BlockContentInputBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; diff --git a/packages/mapi-client/src/generated/types/mapi-story.ts b/packages/mapi-client/src/generated/types/mapi-story.ts index 9a3344ce8..6dd716463 100644 --- a/packages/mapi-client/src/generated/types/mapi-story.ts +++ b/packages/mapi-client/src/generated/types/mapi-story.ts @@ -5,10 +5,10 @@ import type { MapiStory as MapiStoryGenerated, StoryCreate as StoryCreateGenerated, StoryUpdate as StoryUpdateGenerated, -} from './_sources'; -import type { Override, Prettify } from './_utils'; -import type { Block, RootBlock } from './block'; -import type { BlockContent, BlockContentInput } from './field'; +} from "./_sources"; +import type { Override, Prettify } from "./_utils"; +import type { Block, RootBlock } from "./block"; +import type { BlockContent, BlockContentInput } from "./field"; /** * Registry of all blocks, threaded through to resolve nested `bloks` fields. @@ -28,11 +28,14 @@ type MapiStoryWithSchemaContent< TFieldPlugins = Record, > = RootBlock extends TBlock ? TStory - : Override - : BlockContent; - }>; + : Override< + TStory, + { + content: TStory extends StoryCreateGenerated | StoryUpdateGenerated + ? BlockContentInput + : BlockContent; + } + >; type MakeMapiStory< TStory extends MapiStoryGenerated | StoryCreateGenerated | StoryUpdateGenerated, @@ -43,11 +46,21 @@ type MakeMapiStory< // caller passed root block(s) directly → use them as the content type [TBlockOrBlocks] extends [RootBlock] ? MapiStoryWithSchemaContent - // caller passed the full block union → derive root blocks, thread the union as the registry - : [TBlocks] extends [NoBlocks] - ? MapiStoryWithSchemaContent, TBlockOrBlocks, TFieldPlugins> - // caller passed both → honour the explicit registry - : MapiStoryWithSchemaContent, TBlocks, TFieldPlugins> + : // caller passed the full block union → derive root blocks, thread the union as the registry + [TBlocks] extends [NoBlocks] + ? MapiStoryWithSchemaContent< + TStory, + Extract, + TBlockOrBlocks, + TFieldPlugins + > + : // caller passed both → honour the explicit registry + MapiStoryWithSchemaContent< + TStory, + Extract, + TBlocks, + TFieldPlugins + > >; /** A Storyblok MAPI story. */ diff --git a/packages/mapi-client/src/generated/types/story.ts b/packages/mapi-client/src/generated/types/story.ts index d32a8fa23..6021a2a00 100644 --- a/packages/mapi-client/src/generated/types/story.ts +++ b/packages/mapi-client/src/generated/types/story.ts @@ -1,10 +1,10 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { CapiStory as CapiStoryGenerated } from './_sources'; -import type { Override, Prettify } from './_utils'; -import type { Block, RootBlock } from './block'; -import type { BlockContent } from './field'; +import type { CapiStory as CapiStoryGenerated } from "./_sources"; +import type { Override, Prettify } from "./_utils"; +import type { Block, RootBlock } from "./block"; +import type { BlockContent } from "./field"; /** * Registry of all blocks, threaded through to resolve nested `bloks` fields. @@ -27,9 +27,13 @@ export type Story< // caller passed root block(s) directly → use them as the content type [TBlockOrBlocks] extends [RootBlock] ? CapiStoryWithSchemaContent - // caller passed the full block union → derive root blocks, thread the union as the registry - : [TBlocks] extends [NoBlocks] - ? CapiStoryWithSchemaContent, TBlockOrBlocks, TFieldPlugins> - // caller passed both → honour the explicit registry - : CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> + : // caller passed the full block union → derive root blocks, thread the union as the registry + [TBlocks] extends [NoBlocks] + ? CapiStoryWithSchemaContent< + Extract, + TBlockOrBlocks, + TFieldPlugins + > + : // caller passed both → honour the explicit registry + CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> >; diff --git a/packages/richtext/src/generated/overlay/types.gen.ts b/packages/richtext/src/generated/overlay/types.gen.ts index f46f57510..bccdd3783 100644 --- a/packages/richtext/src/generated/overlay/types.gen.ts +++ b/packages/richtext/src/generated/overlay/types.gen.ts @@ -9,16 +9,16 @@ export type RichTextMark = RichTextFieldValueRichTextMark; /** * Rich text field type - structured rich text document (ProseMirror/Tiptap format) */ -export interface RichTextFieldValueRoot { - /** - * Root node type — always "doc" - */ - type: 'doc'; - /** - * Top-level rich text nodes - */ - content: Array; -} +export type RichTextFieldValueRoot = { + /** + * Root node type — always "doc" + */ + type: 'doc'; + /** + * Top-level rich text nodes + */ + content: Array; +}; /** * A rich text document node @@ -30,389 +30,389 @@ export type RichTextFieldValueRichTextNode = RichTextFieldValueParagraphNode | R */ export type RichTextFieldValueRichTextMark = RichTextFieldValueLinkMark | RichTextFieldValueBoldMark | RichTextFieldValueItalicMark | RichTextFieldValueStrikeMark | RichTextFieldValueUnderlineMark | RichTextFieldValueCodeMark | RichTextFieldValueSuperscriptMark | RichTextFieldValueSubscriptMark | RichTextFieldValueHighlightMark | RichTextFieldValueTextStyleMark | RichTextFieldValueAnchorMark | RichTextFieldValueStyledMark; -export interface RichTextFieldValueParagraphNode { - type: 'paragraph'; - attrs?: { - /** - * Text alignment - */ - textAlign: 'left' | 'center' | 'right' | 'justify' | null; +export type RichTextFieldValueParagraphNode = { + type: 'paragraph'; + attrs?: { + /** + * Text alignment + */ + textAlign: 'left' | 'center' | 'right' | 'justify' | null; + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; + +export type RichTextFieldValueTextNode = { + type: 'text'; /** - * Text direction + * The text content */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} + text: string; + marks?: Array; +}; -export interface RichTextFieldValueTextNode { - type: 'text'; - /** - * The text content - */ - text: string; - marks?: Array; -} +export type RichTextFieldValueHeadingNode = { + type: 'heading'; + attrs: { + /** + * Heading level (h1–h6) + */ + level: 1 | 2 | 3 | 4 | 5 | 6 | null; + /** + * Text alignment + */ + textAlign: 'left' | 'center' | 'right' | 'justify' | null; + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueHeadingNode { - type: 'heading'; - attrs: { - /** - * Heading level (h1–h6) - */ - level: 1 | 2 | 3 | 4 | 5 | 6 | null; - /** - * Text alignment - */ - textAlign: 'left' | 'center' | 'right' | 'justify' | null; - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueBlockquoteNode = { + type: 'blockquote'; + attrs?: { + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueBlockquoteNode { - type: 'blockquote'; - attrs?: { - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueBulletListNode = { + type: 'bullet_list'; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueBulletListNode { - type: 'bullet_list'; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueOrderedListNode = { + type: 'ordered_list'; + attrs: { + /** + * Starting number for the ordered list + */ + order?: number; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueOrderedListNode { - type: 'ordered_list'; - attrs: { - /** - * Starting number for the ordered list - */ - order?: number; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueListItemNode = { + type: 'list_item'; + attrs?: { + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueListItemNode { - type: 'list_item'; - attrs?: { - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueCodeBlockNode = { + type: 'code_block'; + attrs: { + /** + * Language class (e.g. "language-typescript") + */ + class: string | null; + /** + * Text direction + */ + dir?: 'ltr' | 'rtl' | null; + }; + content?: Array; + marks?: Array; +}; -export interface RichTextFieldValueCodeBlockNode { - type: 'code_block'; - attrs: { - /** - * Language class (e.g. "language-typescript") - */ - class: string | null; - /** - * Text direction - */ - dir?: 'ltr' | 'rtl' | null; - }; - content?: Array; - marks?: Array; -} +export type RichTextFieldValueHardBreakNode = { + type: 'hard_break'; +}; -export interface RichTextFieldValueHardBreakNode { - type: 'hard_break'; -} +export type RichTextFieldValueHorizontalRuleNode = { + type: 'horizontal_rule'; +}; -export interface RichTextFieldValueHorizontalRuleNode { - type: 'horizontal_rule'; -} +export type RichTextFieldValueImageNode = { + type: 'image'; + attrs: { + /** + * Storyblok asset ID + */ + id: number | null; + /** + * Image URL + */ + src: string | null; + /** + * Alternative text + */ + alt: string | null; + title: string | null; + source: string | null; + copyright: string | null; + /** + * Asset metadata + */ + meta_data: { + alt?: string | null; + title?: string | null; + source?: string | null; + copyright?: string | null; + [key: string]: string | null | string | null | string | null | string | null | string | null | undefined; + } | null; + }; +}; -export interface RichTextFieldValueImageNode { - type: 'image'; - attrs: { - /** - * Storyblok asset ID - */ - id: number | null; - /** - * Image URL - */ - src: string | null; - /** - * Alternative text - */ - alt: string | null; - title: string | null; - source: string | null; - copyright: string | null; - /** - * Asset metadata - */ - meta_data: { - alt?: string | null; - title?: string | null; - source?: string | null; - copyright?: string | null; - [key: string]: string | null | string | null | string | null | string | null | string | null | undefined; - } | null; - }; -} +export type RichTextFieldValueEmojiNode = { + type: 'emoji'; + attrs: { + /** + * Emoji name/slug + */ + name: string; + /** + * Emoji character + */ + emoji: string; + /** + * URL to a fallback image for unsupported environments + */ + fallbackImage: string; + }; +}; -export interface RichTextFieldValueEmojiNode { - type: 'emoji'; - attrs: { - /** - * Emoji name/slug - */ - name: string; - /** - * Emoji character - */ - emoji: string; - /** - * URL to a fallback image for unsupported environments - */ - fallbackImage: string; - }; -} +export type RichTextFieldValueTableNode = { + type: 'table'; + content?: Array; +}; + +export type RichTextFieldValueTableRowNode = { + type: 'tableRow'; + content?: Array; +}; + +export type RichTextFieldValueTableCellNode = { + type: 'tableCell'; + attrs: { + colspan?: number; + rowspan?: number; + /** + * Column widths in pixels + */ + colwidth?: Array | null; + backgroundColor?: string | null; + }; + content?: Array; +}; + +export type RichTextFieldValueTableHeaderNode = { + type: 'tableHeader'; + attrs: { + colspan?: number; + rowspan?: number; + /** + * Column widths in pixels + */ + colwidth?: Array | null; + }; + content?: Array; +}; + +export type RichTextFieldValueBlockNode = { + type: 'blok'; + attrs: { + /** + * Block instance ID + */ + id: string | null; + /** + * Array of embedded component instances + */ + body: Array | null; + }; +}; -export interface RichTextFieldValueTableNode { - type: 'table'; - content?: Array; -} +export type RichTextFieldValueLinkMark = { + type: 'link'; + /** + * Link attributes + */ + attrs: { + /** + * Link URL + */ + href: string | null; + /** + * UUID of the linked story (for internal links) + */ + uuid: string | null; + /** + * Anchor/fragment identifier + */ + anchor: string | null; + /** + * Link target attribute + */ + target: '_self' | '_blank' | '_parent' | '_top' | null; + /** + * Type of link + */ + linktype: 'story' | 'url' | 'email' | 'asset' | null; + /** + * Custom link attributes + */ + custom?: { + [key: string]: unknown; + } | null; + }; +}; + +export type RichTextFieldValueBoldMark = { + type: 'bold'; +}; + +export type RichTextFieldValueItalicMark = { + type: 'italic'; +}; + +export type RichTextFieldValueStrikeMark = { + type: 'strike'; +}; + +export type RichTextFieldValueUnderlineMark = { + type: 'underline'; +}; + +export type RichTextFieldValueCodeMark = { + type: 'code'; +}; + +export type RichTextFieldValueSuperscriptMark = { + type: 'superscript'; +}; + +export type RichTextFieldValueSubscriptMark = { + type: 'subscript'; +}; -export interface RichTextFieldValueTableRowNode { - type: 'tableRow'; - content?: Array; -} +export type RichTextFieldValueHighlightMark = { + type: 'highlight'; + attrs: { + /** + * Highlight color (CSS color value) + */ + color: string | null; + }; +}; + +export type RichTextFieldValueTextStyleMark = { + type: 'textStyle'; + attrs: { + color?: string | null; + id?: string | null; + class?: string | null; + }; +}; + +export type RichTextFieldValueAnchorMark = { + type: 'anchor'; + attrs: { + /** + * Anchor identifier + */ + id: string; + }; +}; -export interface RichTextFieldValueTableCellNode { - type: 'tableCell'; - attrs: { - colspan?: number; - rowspan?: number; +export type RichTextFieldValueStyledMark = { + type: 'styled'; + attrs: { + /** + * CSS class name + */ + class: string | null; + }; +}; + +/** + * Content object for creating or updating a component instance. Contains a component technical name and dynamic fields whose values depend on the component's schema field types. The _uid is optional — Storyblok will auto-generate one if not provided. + */ +export type BlockContentInputRoot = { /** - * Column widths in pixels + * Unique identifier for this component instance (auto-generated when omitted) */ - colwidth?: Array | null; - backgroundColor?: string | null; - }; - content?: Array; -} - -export interface RichTextFieldValueTableHeaderNode { - type: 'tableHeader'; - attrs: { - colspan?: number; - rowspan?: number; + _uid?: string; /** - * Column widths in pixels + * Technical name of the component (content type or nestable block) */ - colwidth?: Array | null; - }; - content?: Array; -} + component: string; + /** + * Storyblok editor markup string for inline editing (present in draft/preview mode) + */ + _editable?: string; + [key: string]: null | string | number | boolean | Array | AssetFieldValueRoot | MultilinkFieldValueRoot | TableFieldValueRoot | RichTextFieldValueRoot | PluginFieldValueRoot | string | undefined; +}; -export interface RichTextFieldValueBlockNode { - type: 'blok'; - attrs: { +/** + * Asset field type - single asset (image, video, audio, or document) + */ +export type AssetFieldValueRoot = { /** - * Block instance ID + * Identifies this as an asset field */ - id: string | null; + fieldtype: 'asset'; /** - * Array of embedded component instances + * Numeric ID of the asset */ - body: Array | null; - }; -} - -export interface RichTextFieldValueLinkMark { - type: 'link'; - /** - * Link attributes - */ - attrs: { + id: number | null; /** - * Link URL + * Alternative text for the asset */ - href: string | null; + alt: string | null; /** - * UUID of the linked story (for internal links) + * Name of the asset */ - uuid: string | null; + name?: string; /** - * Anchor/fragment identifier + * Focal point coordinates for cropping (format "123x456:789x1011") */ - anchor: string | null; + focus?: string | null; /** - * Link target attribute + * Title of the asset */ - target: '_self' | '_blank' | '_parent' | '_top' | null; + title?: string | null; /** - * Type of link + * Source attribution for the asset */ - linktype: 'story' | 'url' | 'email' | 'asset' | null; + source?: string | null; /** - * Custom link attributes + * URL to the asset file */ - custom?: { - [key: string]: unknown; - } | null; - }; -} - -export interface RichTextFieldValueBoldMark { - type: 'bold'; -} - -export interface RichTextFieldValueItalicMark { - type: 'italic'; -} - -export interface RichTextFieldValueStrikeMark { - type: 'strike'; -} - -export interface RichTextFieldValueUnderlineMark { - type: 'underline'; -} - -export interface RichTextFieldValueCodeMark { - type: 'code'; -} - -export interface RichTextFieldValueSuperscriptMark { - type: 'superscript'; -} - -export interface RichTextFieldValueSubscriptMark { - type: 'subscript'; -} - -export interface RichTextFieldValueHighlightMark { - type: 'highlight'; - attrs: { + filename: string; /** - * Highlight color (CSS color value) + * Copyright information */ - color: string | null; - }; -} - -export interface RichTextFieldValueTextStyleMark { - type: 'textStyle'; - attrs: { - color?: string | null; - id?: string | null; - class?: string | null; - }; -} - -export interface RichTextFieldValueAnchorMark { - type: 'anchor'; - attrs: { + copyright?: string | null; /** - * Anchor identifier + * Additional metadata for the asset */ - id: string; - }; -} - -export interface RichTextFieldValueStyledMark { - type: 'styled'; - attrs: { + meta_data?: { + [key: string]: unknown; + }; /** - * CSS class name + * Whether the asset is hosted externally */ - class: string | null; - }; -} - -/** - * Content object for creating or updating a component instance. Contains a component technical name and dynamic fields whose values depend on the component's schema field types. The _uid is optional — Storyblok will auto-generate one if not provided. - */ -export interface BlockContentInputRoot { - /** - * Unique identifier for this component instance (auto-generated when omitted) - */ - _uid?: string; - /** - * Technical name of the component (content type or nestable block) - */ - component: string; - /** - * Storyblok editor markup string for inline editing (present in draft/preview mode) - */ - _editable?: string; - [key: string]: null | string | number | boolean | Array | AssetFieldValueRoot | MultilinkFieldValueRoot | TableFieldValueRoot | RichTextFieldValueRoot | PluginFieldValueRoot | string | undefined; -} - -/** - * Asset field type - single asset (image, video, audio, or document) - */ -export interface AssetFieldValueRoot { - /** - * Identifies this as an asset field - */ - fieldtype: 'asset'; - /** - * Numeric ID of the asset - */ - id: number | null; - /** - * Alternative text for the asset - */ - alt: string | null; - /** - * Name of the asset - */ - name?: string; - /** - * Focal point coordinates for cropping (format "123x456:789x1011") - */ - focus?: string | null; - /** - * Title of the asset - */ - title?: string | null; - /** - * Source attribution for the asset - */ - source?: string | null; - /** - * URL to the asset file - */ - filename: string; - /** - * Copyright information - */ - copyright?: string | null; - /** - * Additional metadata for the asset - */ - meta_data?: { - [key: string]: unknown; - }; - /** - * Whether the asset is hosted externally - */ - is_external_url?: boolean; -} + is_external_url?: boolean; +}; /** * Multilink field type - link to internal stories, external URLs, emails, or assets. @@ -422,132 +422,132 @@ export type MultilinkFieldValueRoot = MultilinkFieldValueStoryLink | MultilinkFi /** * Table field type - structured table data */ -export interface TableFieldValueRoot { - /** - * Table header cells - */ - thead: Array<{ - _uid?: string; - component?: '_table_head'; +export type TableFieldValueRoot = { /** - * Header cell content + * Table header cells */ - value?: string; - }>; - /** - * Table body rows - */ - tbody: Array<{ - _uid?: string; - component?: '_table_row'; + thead: Array<{ + _uid?: string; + component?: '_table_head'; + /** + * Header cell content + */ + value?: string; + }>; /** - * Cells in this row - */ - body?: Array<{ - _uid?: string; - component?: '_table_col'; - /** - * Cell content - */ - value?: string; + * Table body rows + */ + tbody: Array<{ + _uid?: string; + component?: '_table_row'; + /** + * Cells in this row + */ + body?: Array<{ + _uid?: string; + component?: '_table_col'; + /** + * Cell content + */ + value?: string; + }>; }>; - }>; -} +}; /** * Plugin/Custom field type - field plugin with custom structure */ -export interface PluginFieldValueRoot { - /** - * Technical name of the field plugin - */ - plugin: string; - /** - * Unique identifier for this plugin field instance - */ - _uid?: string; - [key: string]: unknown | string | undefined; -} +export type PluginFieldValueRoot = { + /** + * Technical name of the field plugin + */ + plugin: string; + /** + * Unique identifier for this plugin field instance + */ + _uid?: string; + [key: string]: unknown | string | undefined; +}; /** * Link to an internal Storyblok story. */ export type MultilinkFieldValueStoryLink = MultilinkFieldValueSharedLink & { - linktype: 'story'; - /** - * Anchor/fragment identifier for the story link - */ - anchor?: string | null; - /** - * Link relationship attribute - */ - rel?: string; - /** - * Link title attribute - */ - title?: string; + linktype: 'story'; + /** + * Anchor/fragment identifier for the story link + */ + anchor?: string | null; + /** + * Link relationship attribute + */ + rel?: string; + /** + * Link title attribute + */ + title?: string; } & { - [key: string]: string | null; + [key: string]: string | null; }; /** * Link to an external URL. */ export type MultilinkFieldValueUrlLink = MultilinkFieldValueSharedLink & { - linktype: 'url'; - /** - * Link relationship attribute - */ - rel?: string; - /** - * Link title attribute - */ - title?: string; + linktype: 'url'; + /** + * Link relationship attribute + */ + rel?: string; + /** + * Link title attribute + */ + title?: string; } & { - [key: string]: string; + [key: string]: string; }; /** * Link to an email address. */ export type MultilinkFieldValueEmailLink = MultilinkFieldValueSharedLink & { - linktype: 'email'; - /** - * Email address - */ - email?: string; + linktype: 'email'; + /** + * Email address + */ + email?: string; }; /** * Link to a Storyblok asset. */ export type MultilinkFieldValueAssetLink = MultilinkFieldValueSharedLink & { - linktype: 'asset'; -}; - -export interface MultilinkFieldValueSharedLink { - /** - * Identifies this as a multilink field - */ - fieldtype: 'multilink'; - /** - * Type of link - */ - linktype: string; - /** - * UUID of the linked story for internal links - */ - id: string; - /** - * URL for external links - */ - url: string; - /** - * Cached URL path for the linked story - */ - cached_url: string; - /** - * Link target attribute - */ - target?: '_self' | '_blank'; -} + linktype: 'asset'; +}; + +export type MultilinkFieldValueSharedLink = { + /** + * Identifies this as a multilink field + */ + fieldtype: 'multilink'; + /** + * Type of link + */ + linktype: string; + /** + * UUID of the linked story for internal links + */ + id: string; + /** + * URL for external links + */ + url: string; + /** + * Cached URL path for the linked story + */ + cached_url: string; + /** + * Link target attribute + */ + target?: '_self' | '_blank'; +}; diff --git a/packages/richtext/src/static/generate/richtext-element-types.test.ts b/packages/richtext/src/static/generate/richtext-element-types.test.ts new file mode 100644 index 000000000..fbe732dbc --- /dev/null +++ b/packages/richtext/src/static/generate/richtext-element-types.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { generateElementTypesFromSource } from "./richtext-element-types"; + +const asInterfaces = ` +export interface RichTextFieldValueParagraphNode { + type: 'paragraph'; + attrs?: { + textAlign: 'left' | 'center' | 'right' | 'justify' | null; + }; + content?: Array; + marks?: Array; +} + +export interface RichTextFieldValueTextNode { + type: 'text'; + text: string; + marks?: Array; +} +`; + +const asTypeAliases = ` +export type RichTextFieldValueParagraphNode = { + type: 'paragraph'; + attrs?: { + textAlign: 'left' | 'center' | 'right' | 'justify' | null; + }; + content?: Array; + marks?: Array; +}; + +export type RichTextFieldValueTextNode = { + type: 'text'; + text: string; + marks?: Array; +}; +`; + +describe("generateElementTypesFromSource", () => { + it("should map interface declarations to element types", () => { + const result = generateElementTypesFromSource(asInterfaces); + + expect(result).toContain( + "export interface StoryblokRichTextElementByType {", + ); + expect(result).toContain("paragraph: {"); + expect(result).toContain("attrs?: RichTextFieldValueParagraphNode['attrs'];"); + expect(result).toContain("content?: RichTextNode[];"); + expect(result).toContain("text: {"); + expect(result).toContain("text: string;"); + }); + + it("should map type alias declarations the same way as interfaces", () => { + expect(generateElementTypesFromSource(asTypeAliases)).toBe( + generateElementTypesFromSource(asInterfaces), + ); + }); + + it("should ignore declarations outside the RichTextFieldValue namespace", () => { + const result = generateElementTypesFromSource( + `${asInterfaces}\nexport type SomethingElse = { type: 'other' };\n`, + ); + + expect(result).not.toContain("other: {"); + }); + + it("should ignore declarations that are not exported", () => { + const result = generateElementTypesFromSource( + `${asInterfaces}\ntype RichTextFieldValueInternalNode = { type: 'internal' };\n`, + ); + + expect(result).not.toContain("internal: {"); + }); + + it("should throw instead of emitting an empty element type map", () => { + expect(() => + generateElementTypesFromSource("export type Unrelated = { a: string };", "types.gen.ts"), + ).toThrow(/would be empty/); + }); +}); diff --git a/packages/richtext/src/static/generate/richtext-element-types.ts b/packages/richtext/src/static/generate/richtext-element-types.ts index c536b3081..6109af910 100644 --- a/packages/richtext/src/static/generate/richtext-element-types.ts +++ b/packages/richtext/src/static/generate/richtext-element-types.ts @@ -16,28 +16,50 @@ interface NodeEntry { extraProps: Array<{ name: string; optional: boolean; typeText: string }>; } -function parseNodes(inputPath: string): NodeEntry[] { - const source = readFileSync(inputPath, "utf-8"); +/** + * Object declarations the generator understands. `@hey-api/openapi-ts` has no + * option to control whether it emits `export interface X { … }` or + * `export type X = { … }`, and it has switched between the two across releases. + * Both are the same shape for our purposes, so match either instead of pinning + * ourselves to the style of whichever version is currently installed. + */ +function readObjectDeclaration( + stmt: ts.Statement, +): { name: string; members: ts.NodeArray } | undefined { + const isExported = ts.canHaveModifiers(stmt) + ? (ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false) + : false; + if (!isExported) { + return undefined; + } + + if (ts.isInterfaceDeclaration(stmt)) { + return { name: stmt.name.text, members: stmt.members }; + } + if (ts.isTypeAliasDeclaration(stmt) && ts.isTypeLiteralNode(stmt.type)) { + return { name: stmt.name.text, members: stmt.type.members }; + } + return undefined; +} + +function parseNodes(source: string): NodeEntry[] { const sf = ts.createSourceFile("types.gen.ts", source, ts.ScriptTarget.Latest, true); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); const entries: NodeEntry[] = []; for (const stmt of sf.statements) { - if (!ts.isInterfaceDeclaration(stmt)) { + const declaration = readObjectDeclaration(stmt); + if (!declaration) { continue; } - const isExported = stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false; - if (!isExported) { - continue; - } - if (!stmt.name.text.startsWith("RichTextFieldValue")) { + if (!declaration.name.startsWith("RichTextFieldValue")) { continue; } // Find type: 'literal' let typeValue: string | undefined; - for (const m of stmt.members) { + for (const m of declaration.members) { if ( ts.isPropertySignature(m) && m.name && @@ -63,7 +85,7 @@ function parseNodes(inputPath: string): NodeEntry[] { let marksOptional = false; const extraProps: NodeEntry["extraProps"] = []; - for (const m of stmt.members) { + for (const m of declaration.members) { if (!ts.isPropertySignature(m) || !m.name || !ts.isIdentifier(m.name)) { continue; } @@ -94,7 +116,7 @@ function parseNodes(inputPath: string): NodeEntry[] { entries.push({ typeValue, - ifaceName: stmt.name.text, + ifaceName: declaration.name, hasAttrs, attrsOptional, hasContent, @@ -109,7 +131,23 @@ function parseNodes(inputPath: string): NodeEntry[] { } export function generateElementTypes(inputPath: string): string { - const nodes = parseNodes(inputPath); + return generateElementTypesFromSource(readFileSync(inputPath, "utf-8"), inputPath); +} + +export function generateElementTypesFromSource( + source: string, + sourceName = "types.gen.ts", +): string { + const nodes = parseNodes(source); + + // Without this the generator writes an empty `StoryblokRichTextElementByType` + // and exits successfully, which only surfaces much later as a type error in a + // consumer package. + if (nodes.length === 0) { + throw new Error( + `No exported RichTextFieldValue* object declarations found in ${sourceName}. The generated element types would be empty.`, + ); + } // Import only the interfaces that have attrs (we reference their ['attrs'] type) const ifaceImports = nodes diff --git a/packages/schema/src/generated/types/block.ts b/packages/schema/src/generated/types/block.ts index 698932b77..f361ba9cf 100644 --- a/packages/schema/src/generated/types/block.ts +++ b/packages/schema/src/generated/types/block.ts @@ -1,11 +1,8 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { - Component as ComponentGenerated, - Field, -} from './_sources'; -import type { Override } from './_utils'; +import type { Component as ComponentGenerated, Field } from "./_sources"; +import type { Override } from "./_utils"; /** * Ordered array of named fields — the content-shape DSL form `defineBlock` @@ -23,42 +20,46 @@ export type Block< TFields extends BlockFields = BlockFields, TIsRoot extends boolean = boolean, TIsNestable extends boolean = boolean, -> = Override, { - name: TName; - fields: TFields; - is_root?: TIsRoot; - is_nestable?: TIsNestable; - /** - * Escape hatch for pinning this block to a Storyblok UI-managed component - * group by UUID. Component groups are normally maintained in code via the - * schema directory layout; set this only if you intentionally manage groups - * in the Storyblok UI, and fill in the group UUID yourself. When set, - * `schema push` diffs it and sends it to the Management API; when omitted, - * the block's remote group is left untouched. - * - * @deprecated Prefer maintaining component groups in code through the - * directory layout. - */ - component_group_uuid?: string | null; - /** - * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = - * explicitly ungrouped (push clears the group). Absent = unmanaged (push - * leaves the remote group untouched). - */ - folder?: string | null; -}>; +> = Override< + Omit, + { + name: TName; + fields: TFields; + is_root?: TIsRoot; + is_nestable?: TIsNestable; + /** + * Escape hatch for pinning this block to a Storyblok UI-managed component + * group by UUID. Component groups are normally maintained in code via the + * schema directory layout; set this only if you intentionally manage groups + * in the Storyblok UI, and fill in the group UUID yourself. When set, + * `schema push` diffs it and sends it to the Management API; when omitted, + * the block's remote group is left untouched. + * + * @deprecated Prefer maintaining component groups in code through the + * directory layout. + */ + component_group_uuid?: string | null; + /** + * Folder membership as a display name path (e.g. `'Layout/Heros'`); `null` = + * explicitly ungrouped (push clears the group). Absent = unmanaged (push + * leaves the remote group untouched). + */ + folder?: string | null; + } +>; /** * A root {@link Block} (`is_root: true`). Given a union of blocks, narrows to * its root members; with no argument it is the generic root-block type. */ -export type RootBlock = - Extract; +export type RootBlock = Extract; /** * A nestable {@link Block} (`is_nestable: true`). Given a union of blocks, * narrows to its nestable members; with no argument it is the generic * nestable-block type. */ -export type NestableBlock = - Extract; +export type NestableBlock = Extract< + T, + { is_nestable: true } +>; diff --git a/packages/schema/src/generated/types/field.ts b/packages/schema/src/generated/types/field.ts index 29182b09b..870837962 100644 --- a/packages/schema/src/generated/types/field.ts +++ b/packages/schema/src/generated/types/field.ts @@ -10,12 +10,18 @@ import type { PluginFieldValue, RichTextFieldValue, TableFieldValue, -} from './_sources'; -import type { Prettify } from './_utils'; -import type { Block, BlockFields } from './block'; +} from "./_sources"; +import type { Prettify } from "./_utils"; +import type { Block, BlockFields } from "./block"; export type { Field }; -export type { AssetFieldValue, MultilinkFieldValue, PluginFieldValue, RichTextFieldValue, TableFieldValue }; +export type { + AssetFieldValue, + MultilinkFieldValue, + PluginFieldValue, + RichTextFieldValue, + TableFieldValue, +}; /** * @deprecated Use {@link RichTextFieldValue} instead. Will be removed in a future major version. @@ -37,15 +43,45 @@ type IsBaseBlock = [Block] extends [T] ? true : false; * required (`required: true`) from optional fields. Each `F` is a member of the * field union, so it provably satisfies `FieldValue`'s `Field` constraint. */ -type ContentFields> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValue } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValue | null } +type ContentFields< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValue< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValue< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** Input (write) variant of {@link ContentFields}, resolving each field via {@link FieldValueInput}. */ -type ContentFieldsInput> = Prettify< - { [F in TFields[number] as F extends { required: true } ? F['name'] : never]: FieldValueInput } - & { [F in TFields[number] as F extends { required: true } ? never : F['name']]?: FieldValueInput | null } +type ContentFieldsInput< + TFields extends BlockFields, + TBlocks extends Block | NoBlocks, + TFieldPlugins = Record, +> = Prettify< + { + [F in TFields[number] as F extends { required: true } ? F["name"] : never]: FieldValueInput< + F, + TBlocks, + TFieldPlugins + >; + } & { + [F in TFields[number] as F extends { required: true } ? never : F["name"]]?: FieldValueInput< + F, + TBlocks, + TFieldPlugins + > | null; + } >; /** @@ -54,27 +90,41 @@ type ContentFieldsInput> = +export type BlockContent< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid: string; component: TBlock['name']; _editable?: string } - & ContentFields - > + { _uid: string; component: TBlock["name"]; _editable?: string } & ContentFields< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; /** Input variant of {@link BlockContent} for write operations (creating/updating stories via the MAPI). `_uid` is optional. */ -export type BlockContentInput> = +export type BlockContentInput< + TBlock extends Block = Block, + TBlocks extends Block | NoBlocks = NoBlocks, + TFieldPlugins = Record, +> = IsBaseBlock extends true ? BlockContentInputBase - // distribute over each member of the `TBlock` union - : TBlock extends any + : // distribute over each member of the `TBlock` union + TBlock extends any ? Prettify< - { _uid?: string; component: TBlock['name']; _editable?: string } - & ContentFieldsInput - > + { _uid?: string; component: TBlock["name"]; _editable?: string } & ContentFieldsInput< + TBlock["fields"], + TBlocks, + TFieldPlugins + > + > : never; export type BlocksFieldValue< @@ -84,7 +134,7 @@ export type BlocksFieldValue< > = BlockContent[]; /** Union of all valid Storyblok field type discriminants (e.g., `text`, `bloks`). */ -export type FieldType = Field['type']; +export type FieldType = Field["type"]; interface FieldTypeValueMap { text: string; @@ -112,10 +162,11 @@ interface FieldTypeValueMap { custom: PluginFieldValue; } -type IsNestable = - T extends { is_nestable: false } ? false - : T extends { is_nestable: true } ? true - : true; +type IsNestable = T extends { is_nestable: false } + ? false + : T extends { is_nestable: true } + ? true + : true; type AllowEntry = string | { folder: string }; @@ -130,22 +181,31 @@ type AllowEntry = string | { folder: string }; * string path on both the block's `folder` and the field's `allow`: a shared ref * carries the exact path on both sides, so no drift is possible. */ -type MatchesFolder = - TBlock extends { folder: infer BF extends string } - ? Lowercase extends Lowercase | `${Lowercase}/${string}` ? TBlock : never - : never; +type MatchesFolder = TBlock extends { + folder: infer BF extends string; +} + ? Lowercase extends Lowercase | `${Lowercase}/${string}` + ? TBlock + : never + : never; -type ApplyAllow = TField extends { allow: ReadonlyArray } +type ApplyAllow = TField extends { + allow: ReadonlyArray; +} ? TAllowed extends string - // keep only the registry blocks named in `allow` - ? Extract + ? // keep only the registry blocks named in `allow` + Extract : TAllowed extends { folder: infer F extends string } - // keep registry blocks in the folder (or any nested folder) - ? TBlocks extends any ? MatchesFolder : never + ? // keep registry blocks in the folder (or any nested folder) + TBlocks extends any + ? MatchesFolder + : never + : never + : // no `allow`: distribute over the registry, keeping nestable blocks + TBlocks extends any + ? IsNestable extends true + ? TBlocks : never - // no `allow`: distribute over the registry, keeping nestable blocks - : TBlocks extends any - ? IsNestable extends true ? TBlocks : never : never; /** @@ -154,7 +214,9 @@ type ApplyAllow = TField extends { allow: ReadonlyArray = TField extends { deny: ReadonlyArray } +type ApplyDeny = TField extends { + deny: ReadonlyArray; +} ? Exclude : TBlocks; @@ -170,12 +232,11 @@ type ApplyRestrictions = ApplyDeny = - TField extends { field_type: infer F extends string } - ? F extends keyof TFieldPlugins - ? Prettify - : PluginFieldValue - : PluginFieldValue; +type ResolveCustom = TField extends { field_type: infer F extends string } + ? F extends keyof TFieldPlugins + ? Prettify + : PluginFieldValue + : PluginFieldValue; /** Resolves a field definition to its runtime content value type (read). */ export type FieldValue< @@ -183,17 +244,17 @@ export type FieldValue< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentBase[] - : [TBlocks] extends [Block] - ? BlockContent, TBlocks, TFieldPlugins>[] - : BlockContentBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentBase[] + : [TBlocks] extends [Block] + ? BlockContent, TBlocks, TFieldPlugins>[] + : BlockContentBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; /** Resolves a field definition to its input value type (write). */ @@ -202,15 +263,15 @@ export type FieldValueInput< TBlocks extends Block | NoBlocks = NoBlocks, TFieldPlugins = Record, > = Prettify< - TField extends { type: 'bloks' } - // guard `never` first: `[never] extends [Block]` is structurally true, so an - // empty registry would otherwise be mistaken for a populated one - ? [TBlocks] extends [never] - ? BlockContentInputBase[] - : [TBlocks] extends [Block] - ? BlockContentInput, TBlocks, TFieldPlugins>[] - : BlockContentInputBase[] - : TField extends { type: 'custom' } + TField extends { type: "bloks" } + ? // guard `never` first: `[never] extends [Block]` is structurally true, so an + // empty registry would otherwise be mistaken for a populated one + [TBlocks] extends [never] + ? BlockContentInputBase[] + : [TBlocks] extends [Block] + ? BlockContentInput, TBlocks, TFieldPlugins>[] + : BlockContentInputBase[] + : TField extends { type: "custom" } ? ResolveCustom - : FieldTypeValueMap[TField['type']] + : FieldTypeValueMap[TField["type"]] >; diff --git a/packages/schema/src/generated/types/mapi-story.ts b/packages/schema/src/generated/types/mapi-story.ts index 9a3344ce8..6dd716463 100644 --- a/packages/schema/src/generated/types/mapi-story.ts +++ b/packages/schema/src/generated/types/mapi-story.ts @@ -5,10 +5,10 @@ import type { MapiStory as MapiStoryGenerated, StoryCreate as StoryCreateGenerated, StoryUpdate as StoryUpdateGenerated, -} from './_sources'; -import type { Override, Prettify } from './_utils'; -import type { Block, RootBlock } from './block'; -import type { BlockContent, BlockContentInput } from './field'; +} from "./_sources"; +import type { Override, Prettify } from "./_utils"; +import type { Block, RootBlock } from "./block"; +import type { BlockContent, BlockContentInput } from "./field"; /** * Registry of all blocks, threaded through to resolve nested `bloks` fields. @@ -28,11 +28,14 @@ type MapiStoryWithSchemaContent< TFieldPlugins = Record, > = RootBlock extends TBlock ? TStory - : Override - : BlockContent; - }>; + : Override< + TStory, + { + content: TStory extends StoryCreateGenerated | StoryUpdateGenerated + ? BlockContentInput + : BlockContent; + } + >; type MakeMapiStory< TStory extends MapiStoryGenerated | StoryCreateGenerated | StoryUpdateGenerated, @@ -43,11 +46,21 @@ type MakeMapiStory< // caller passed root block(s) directly → use them as the content type [TBlockOrBlocks] extends [RootBlock] ? MapiStoryWithSchemaContent - // caller passed the full block union → derive root blocks, thread the union as the registry - : [TBlocks] extends [NoBlocks] - ? MapiStoryWithSchemaContent, TBlockOrBlocks, TFieldPlugins> - // caller passed both → honour the explicit registry - : MapiStoryWithSchemaContent, TBlocks, TFieldPlugins> + : // caller passed the full block union → derive root blocks, thread the union as the registry + [TBlocks] extends [NoBlocks] + ? MapiStoryWithSchemaContent< + TStory, + Extract, + TBlockOrBlocks, + TFieldPlugins + > + : // caller passed both → honour the explicit registry + MapiStoryWithSchemaContent< + TStory, + Extract, + TBlocks, + TFieldPlugins + > >; /** A Storyblok MAPI story. */ diff --git a/packages/schema/src/generated/types/story.ts b/packages/schema/src/generated/types/story.ts index d32a8fa23..6021a2a00 100644 --- a/packages/schema/src/generated/types/story.ts +++ b/packages/schema/src/generated/types/story.ts @@ -1,10 +1,10 @@ // Generated by @storyblok/openapi-codegen. Do not edit by hand. // Source template lives in tools/openapi-codegen/templates/. -import type { CapiStory as CapiStoryGenerated } from './_sources'; -import type { Override, Prettify } from './_utils'; -import type { Block, RootBlock } from './block'; -import type { BlockContent } from './field'; +import type { CapiStory as CapiStoryGenerated } from "./_sources"; +import type { Override, Prettify } from "./_utils"; +import type { Block, RootBlock } from "./block"; +import type { BlockContent } from "./field"; /** * Registry of all blocks, threaded through to resolve nested `bloks` fields. @@ -27,9 +27,13 @@ export type Story< // caller passed root block(s) directly → use them as the content type [TBlockOrBlocks] extends [RootBlock] ? CapiStoryWithSchemaContent - // caller passed the full block union → derive root blocks, thread the union as the registry - : [TBlocks] extends [NoBlocks] - ? CapiStoryWithSchemaContent, TBlockOrBlocks, TFieldPlugins> - // caller passed both → honour the explicit registry - : CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> + : // caller passed the full block union → derive root blocks, thread the union as the registry + [TBlocks] extends [NoBlocks] + ? CapiStoryWithSchemaContent< + Extract, + TBlockOrBlocks, + TFieldPlugins + > + : // caller passed both → honour the explicit registry + CapiStoryWithSchemaContent, TBlocks, TFieldPlugins> >; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c69a0ca4..36a52bfa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1725,7 +1725,7 @@ importers: tools/openapi-codegen: dependencies: '@hey-api/openapi-ts': - specifier: ^0.92.3 + specifier: 0.92.4 version: 0.92.4(magicast@0.5.2)(typescript@5.8.3) pathe: specifier: ^2.0.3 diff --git a/tools/openapi-codegen/README.md b/tools/openapi-codegen/README.md index 1b13aba6e..87fddbb16 100644 --- a/tools/openapi-codegen/README.md +++ b/tools/openapi-codegen/README.md @@ -23,6 +23,14 @@ The second source is the easier one to get wrong. Editing a file under `specs/` generated types for every consumer even though `spec.lock` does not move, so there is no lock diff to remind you to regenerate. +There is a third, less obvious input: the generator version itself. `@hey-api/openapi-ts` is a `0.x` +package that changes its output across patch releases — 0.92.4 emits `export type X = { … }` where +earlier releases emitted `export interface X { … }`, and that style is not configurable. Because the +generated output is committed and CI never regenerates it (that needs spec access), a floating range +would let a plain `pnpm install` silently change what the next regeneration produces. So +`@hey-api/openapi-ts` is pinned to an exact version here. Bump it deliberately, regenerate every +consumer in the same PR, and review the diff. + ## Workflow ```sh diff --git a/tools/openapi-codegen/package.json b/tools/openapi-codegen/package.json index 2954e49df..1cebdebe0 100644 --- a/tools/openapi-codegen/package.json +++ b/tools/openapi-codegen/package.json @@ -32,7 +32,7 @@ "test:coverage": "vp test run --coverage" }, "dependencies": { - "@hey-api/openapi-ts": "^0.92.3", + "@hey-api/openapi-ts": "0.92.4", "pathe": "^2.0.3", "typescript": "5.8.3", "yaml": "^2.8.2"