diff --git a/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts b/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts index 55f455d602..bc87cd5c1b 100644 --- a/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts +++ b/packages/super-editor/src/editors/v1/core/types/ChainedCommands.ts @@ -48,42 +48,80 @@ type KnownCommandRecord = { }; /** - * A chainable version of an editor command keyed by command name. + * Union of all command interfaces via explicit imports. + * Module augmentation doesn't survive the npm boundary, so this is the + * single source of truth for the built-in command surface. Used by + * EditorCommands, ChainableCommandObject, and CanObject. */ -export type ChainedCommand = (...args: CommandArgs) => ChainableCommandObject; +type AllCommandSignatures = CoreCommandSignatures & + CommentCommands & + FormattingCommandAugmentations & + HistoryLinkTableCommandAugmentations & + SpecializedCommandAugmentations & + ParagraphCommands & + BlockNodeCommands & + ImageCommands & + MiscellaneousCommands & + TrackChangesCommands; -type KnownChainedCommands = { +/** + * Transforms a command interface so every method returns ChainableCommandObject + * instead of boolean, preserving parameter types. + */ +type Chainified = { + [K in keyof T]: T[K] extends (...args: infer A) => unknown ? (...args: A) => ChainableCommandObject : T[K]; +}; + +/** + * Commands from module augmentation, transformed for chaining. + * Empty for npm consumers (augmentation doesn't survive the boundary), + * but consumers who augment ExtensionCommandMap get their custom commands + * on chain() for free. + */ +type AugmentedChainedCommands = { [K in keyof RegisteredCommands]: (...args: CommandArgs) => ChainableCommandObject; }; +/** Same as AugmentedChainedCommands but with original return types for can(). */ +type AugmentedCanCommands = { + [K in keyof RegisteredCommands]: (...args: CommandArgs) => CommandResult; +}; + +/** + * A chainable version of an editor command keyed by command name. + */ +export type ChainedCommand = (...args: CommandArgs) => ChainableCommandObject; + /** * Chainable command object returned by `createChain`. - * Has dynamic keys (one per command) and a `run()` method. + * Only `run()` returns boolean — all other methods return ChainableCommandObject. + * + * Includes AugmentedChainedCommands so consumers who extend ExtensionCommandMap + * via module augmentation get their custom commands on chain() automatically. */ export type ChainableCommandObject = { run: () => boolean; -} & KnownChainedCommands & - Record ChainableCommandObject>; +} & Chainified & + AugmentedChainedCommands; /** * A command that can be checked for availability. */ export type CanCommand = (...args: CommandArgs) => CommandResult; -type KnownCanCommands = { - [K in keyof RegisteredCommands]: (...args: CommandArgs) => CommandResult; -}; - /** * Map of commands that can be checked. */ export type CanCommands = Record; /** - * Object returned by `createCan`: dynamic boolean commands + a `chain()` helper. + * Object returned by `createCan`: typed boolean commands + a `chain()` helper. + * + * Includes AugmentedCanCommands so consumers who extend ExtensionCommandMap + * via module augmentation get their custom commands on can() automatically. */ -export type CanObject = KnownCanCommands & - Record & { +export type CanObject = AllCommandSignatures & + AugmentedCanCommands & { chain: () => ChainableCommandObject; }; @@ -100,23 +138,11 @@ export type ExtensionCommands = Pick fallback allows dynamic/plugin commands. + * Composed from AllCommandSignatures (explicit imports) for reliable + * cross-package typing, plus CoreCommands/ExtensionCommands (module + * augmentation) and a Record fallback for dynamic/plugin commands. */ -export type EditorCommands = CoreCommands & - ExtensionCommands & - CoreCommandSignatures & - CommentCommands & - FormattingCommandAugmentations & - HistoryLinkTableCommandAugmentations & - SpecializedCommandAugmentations & - ParagraphCommands & - BlockNodeCommands & - ImageCommands & - MiscellaneousCommands & - TrackChangesCommands & - Record; +export type EditorCommands = CoreCommands & ExtensionCommands & AllCommandSignatures & Record; /** * Command props made available to every command handler. diff --git a/tests/consumer-typecheck/src/customer-scenario.ts b/tests/consumer-typecheck/src/customer-scenario.ts index 7e224bbcb0..f2accc2fb0 100644 --- a/tests/consumer-typecheck/src/customer-scenario.ts +++ b/tests/consumer-typecheck/src/customer-scenario.ts @@ -270,6 +270,15 @@ function testEditorCommands(editor: Editor) { // Chain API editor.chain().toggleBold().toggleItalic().run(); + + // SD-2334: Chain intermediate methods must return ChainableCommandObject, not boolean. + // Reproduces IT-344 (Ontra): chain().setTextSelection(...).setMark(...).run() + const chainResult: ChainableCommandObject = editor.chain().setTextSelection({ from: 0, to: 5 }); + const runResult: boolean = editor.chain().setTextSelection({ from: 0, to: 5 }).setMark('bold').run(); + + // SD-2334: can().chain() must return ChainableCommandObject, not boolean + const canChain: ChainableCommandObject = editor.can().chain(); + const canChainRun: boolean = editor.can().chain().toggleBold().run(); } function testPresentationEditorCommands(pe: PresentationEditor) {