Skip to content

fix(types): fix circular deps#1099

Merged
makhnatkin merged 2 commits intomainfrom
fix/types-circular-deps
Apr 24, 2026
Merged

fix(types): fix circular deps#1099
makhnatkin merged 2 commits intomainfrom
fix/types-circular-deps

Conversation

@makhnatkin
Copy link
Copy Markdown
Collaborator

@makhnatkin makhnatkin commented Apr 22, 2026

Summary

Fixes type-only circular imports that were invisible in CI because scripts/check-circular-deps.js ran dpdm with transform: true, stripping import type edges. Drops that flag and refactors the dependency graph so the cycle threshold can be lowered to 0 with types.

Changes

  • CI guard: scripts/check-circular-deps.js is now type-aware (transform: false).
  • Graph cleanup: introduced leaf type modules (bundle/events, bundle/preset-base-types, bundle/editor-public-types, GPT/types, html-to-markdown/types), co-located extension/serializer types with their implementations (ExtensionBuilder.ts, MarkdownSerializer.ts), and replaced barrel imports with direct deep imports on the remaining hot paths.
  • Backward compat: core/types/extension.ts, bundle/types.ts, bundle/Editor.ts and core/markdown/MarkdownSerializerDynamicModifier.ts keep their public shapes as thin re-export facades, so @gravity-ui/markdown-editor/_/* deep imports stay green.

Summary by Sourcery

Make the circular dependency check type-aware and reorganize editor type definitions into dedicated modules to avoid type-only import cycles while preserving the existing public API.

Bug Fixes:

  • Ensure the circular dependency checker inspects type-only imports by disabling source transformation.

Enhancements:

  • Co-locate extension and serializer-related type definitions with their implementations and introduce dedicated public type modules for the editor bundle, GPT integration, HTML-to-Markdown conversion, and widget decoration.
  • Refactor various internal modules to re-export types from the new leaf type modules, keeping external and deep-import APIs stable.
  • Simplify widget decoration metadata handling by inlining the meta payload structure where used.

@makhnatkin makhnatkin requested a review from d3m1d0v as a code owner April 22, 2026 15:57
@makhnatkin makhnatkin marked this pull request as draft April 22, 2026 15:57
sourcery-ai[bot]

This comment was marked as outdated.

@gravity-ui
Copy link
Copy Markdown

gravity-ui Bot commented Apr 22, 2026

Storybook Deployed

@gravity-ui
Copy link
Copy Markdown

gravity-ui Bot commented Apr 22, 2026

🎭 Playwright Report

sourcery-ai[bot]

This comment was marked as off-topic.

@makhnatkin makhnatkin force-pushed the fix/types-circular-deps branch from 172ce6e to efdf942 Compare April 23, 2026 02:31
@makhnatkin makhnatkin changed the title fix: remove transform:true from circular-deps check and fix all revealed type-only cycles fix(types): fix circular deps Apr 23, 2026
sourcery-ai[bot]

This comment was marked as outdated.

@makhnatkin makhnatkin force-pushed the fix/types-circular-deps branch from 20eacd2 to 59fa46f Compare April 23, 2026 11:22
@gravity-ui gravity-ui deleted a comment from sourcery-ai Bot Apr 23, 2026
@makhnatkin makhnatkin force-pushed the fix/types-circular-deps branch from 5b81eb9 to b036c83 Compare April 23, 2026 14:51
@makhnatkin makhnatkin marked this pull request as ready for review April 23, 2026 14:51
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 23, 2026

Reviewer's Guide

Refactors the editor package’s type structure to eliminate type-only circular dependencies while keeping the external public API stable, and tightens the circular-deps CI check to be type-aware by disabling dpdm’s transform step.

File-Level Changes

Change Details Files
Move extension-related type definitions next to ExtensionBuilder implementation and expose them via re-export facades to break cycles.
  • Delete concrete type definitions from core/types/extension.ts and turn it into a pure re-export layer
  • Define Extension, ExtensionAuto, ExtensionWithOptions, ExtensionSpec, ExtensionNodeSpec, ExtensionMarkSpec, and ExtensionDeps inside ExtensionBuilder.ts, including Schema and ActionStorage in ExtensionDeps
  • Update imports across core (ExtensionsManager, tests) to consume types directly from ExtensionBuilder instead of the old types/extension barrel
  • Re-export extension types from core/index.ts via the new location implicitly through ExtensionBuilder usage
packages/editor/src/core/types/extension.ts
packages/editor/src/core/ExtensionBuilder.ts
packages/editor/src/core/ExtensionsManager.ts
packages/editor/src/core/ExtensionBuilder.test.ts
packages/editor/src/core/index.ts
Inline MarkdownSerializerDynamicModifier and related types into MarkdownSerializer to avoid an extra module edge and update all call sites.
  • Replace MarkdownSerializerDynamicModifier.ts implementation with a thin re-export facade that re-exports the class and config type from MarkdownSerializer.ts
  • Move SerializerProcessNode, SerializerNodeProcessor, MarkdownSerializerDynamicModifierConfig, and the MarkdownSerializerDynamicModifier class implementation into MarkdownSerializer.ts
  • Update imports in SerializerTokensRegistry, ExtensionsManager, dynamicModifiers types/utils to refer to './markdown/MarkdownSerializer' instead of './MarkdownSerializerDynamicModifier'
packages/editor/src/core/markdown/MarkdownSerializerDynamicModifier.ts
packages/editor/src/core/markdown/MarkdownSerializer.ts
packages/editor/src/core/SerializerTokensRegistry.ts
packages/editor/src/core/ExtensionsManager.ts
packages/editor/src/core/types/dynamicModifiers.ts
packages/editor/src/core/utils/dynamicModifiers.ts
Factor common GPT widget/dialog types into a shared leaf module to remove bidirectional dependencies between GPT components.
  • Create extensions/additional/GPT/types.ts containing shared GptDialogProps, GptWidgetOptions, and PresetListProps generics plus internal GptAlertProps and shared props helpers
  • Change GptDialog, MarkupGpt popup, useGpt hook, PresetList, gptExtension, and utilities to import/export types from the new types.ts instead of cross-importing each other
  • Simplify GptDialog.tsx and PresetList.tsx to only re-export their props types from the new shared module
packages/editor/src/extensions/additional/GPT/types.ts
packages/editor/src/extensions/additional/GPT/GptDialog/GptDialog.tsx
packages/editor/src/extensions/additional/GPT/PresetList/PresetList.tsx
packages/editor/src/extensions/additional/GPT/gptExtension/gptExtension.ts
packages/editor/src/extensions/additional/GPT/plugin.ts
packages/editor/src/extensions/additional/GPT/MarkupGpt/popup.tsx
packages/editor/src/extensions/additional/GPT/hooks/useGpt.tsx
packages/editor/src/extensions/additional/GPT/hooks/usePresetList.ts
packages/editor/src/extensions/additional/GPT/utils.ts
Introduce leaf type modules for bundle/editor public API and preset options while keeping previous imports working via re-export facades.
  • Add bundle/editor-public-types.ts defining MarkdownEditorInstance interface and ChangeEditorModeOptions based on new EventMap and MarkdownEditorMode types
  • Add bundle/events.ts encapsulating EventMap and ToolbarActionData tied to MarkdownEditorMode
  • Add bundle/preset-base-types.ts for MarkdownEditorMode, MarkdownEditorPreset, MarkdownEditorSplitMode, and WysiwygPlaceholderOptions
  • Refactor bundle/Editor.ts to export Editor as MarkdownEditorInstance and re-export existing ToolbarActionData, EventMap, and ChangeEditorModeOptions from the new leaf modules
  • Refactor bundle/types.ts to import base preset/placeholder types and ChangeEditorModeOptions from the new leaf modules, and to re-export MarkdownEditorInstance, ChangeEditorModeOptions, MarkdownEditorMode/MarkdownEditorPreset/MarkdownEditorSplitMode, WysiwygPlaceholderOptions, and ParseInsertedUrlAsImage as public types
  • Move ParseInsertedUrlAsImage into utils/upload.ts and adapt imports in bundle/types.ts and various extensions (Image paste handlers) accordingly
  • Export ToolbarConfigs type from bundle/toolbar/types and adjust getToolbarsConfigs signature to consume ToolbarConfigs instead of a wide Pick of MarkdownEditorViewProps, while removing the local ToolbarConfigs definition from MarkdownEditorView.tsx
packages/editor/src/bundle/editor-public-types.ts
packages/editor/src/bundle/events.ts
packages/editor/src/bundle/preset-base-types.ts
packages/editor/src/bundle/Editor.ts
packages/editor/src/bundle/types.ts
packages/editor/src/bundle/MarkdownEditorView.tsx
packages/editor/src/bundle/toolbar/types.ts
packages/editor/src/bundle/toolbar/utils/toolbarsConfigs.ts
packages/editor/src/bundle/useMarkdownEditor.ts
packages/editor/src/utils/upload.ts
packages/editor/src/extensions/markdown/Image/imageUrlPaste/index.ts
packages/editor/src/extensions/yfm/ImgSize/ImagePaste/index.ts
packages/editor/src/bundle/config/dynamicModifiers.test.ts
packages/editor/src/bundle/config/wysiwyg.ts
packages/editor/src/core/Editor.ts
packages/editor/src/extensions/additional/GPT/MarkupGpt/index.ts
packages/editor/src/extensions/additional/GPT/MarkupGpt/plugin.ts
packages/editor/src/extensions/additional/GPT/gptExtension/view.tsx
packages/editor/src/markup/codemirror/create.ts
packages/editor/src/markup/codemirror/search-plugin/plugin.ts
Split HTML-to-Markdown visitor interface into a dedicated types module and update dependents to use it as a leaf type-only import.
  • Add markup/codemirror/html-to-markdown/types.ts introducing the HTMLNodeVisitor interface
  • Remove the HTMLNodeVisitor interface definition from converters.ts and instead import it from the new types module
  • Update handlers and other html-to-markdown code to import HTMLNodeVisitor from the new types module where needed
packages/editor/src/markup/codemirror/html-to-markdown/types.ts
packages/editor/src/markup/codemirror/html-to-markdown/converters.ts
packages/editor/src/markup/codemirror/html-to-markdown/handlers.ts
Localize note/deflist extension placeholder option types into their schema modules and re-export via the index modules to remove index<->schema cycles.
  • Define DeflistSpecsOptions in DeflistSpecs/schema.ts and export it, instead of defining it in DeflistSpecs/index.ts
  • Define YfmNoteSpecsOptions in YfmNoteSpecs/schema.ts and export it, instead of defining it in YfmNoteSpecs/index.ts
  • Make the index.ts files re-export these options types from their schema modules so external imports keep working
packages/editor/src/extensions/markdown/Deflist/DeflistSpecs/schema.ts
packages/editor/src/extensions/markdown/Deflist/DeflistSpecs/index.ts
packages/editor/src/extensions/yfm/YfmNote/YfmNoteSpecs/schema.ts
packages/editor/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts
Restructure widget decoration metadata typing so the add/remove meta union is defined next to WidgetDescriptor and re-exported from the shared types module.
  • Move the Meta union type definition from WidgetDecoration/types.ts into WidgetDecoration/WidgetDescriptor.ts and export it there
  • Change WidgetDecoration/types.ts to import and re-export Meta from WidgetDescriptor instead of defining it
  • Inline the Meta type usage in actions.ts to avoid referencing the types module just for metadata shape and simplify removeDecoration
packages/editor/src/extensions/behavior/WidgetDecoration/WidgetDescriptor.ts
packages/editor/src/extensions/behavior/WidgetDecoration/types.ts
packages/editor/src/extensions/behavior/WidgetDecoration/actions.ts
Update React renderer facet typing to depend directly on the ReactRenderer behavior extension leaf module instead of the generic extensions barrel.
  • Change ReactRendererFacet’s ReactRenderStorage import from '../../extensions' to '../../extensions/behavior/ReactRenderer' to avoid pulling the entire extensions index and its dependency graph into the codemirror react-facet module
packages/editor/src/markup/codemirror/react-facet.ts
Inline video action attrs type next to the Video action implementation and re-export it from the public Video extension index to remove a circular type edge.
  • Define VideoActionAttrs in Video/actions.ts based on VideoService and url
  • Re-export VideoActionAttrs from Video/index.ts instead of defining it there, and change actions/index to import the type locally
  • Ensure downstream code consuming VideoActionAttrs continues importing from the extension index
packages/editor/src/extensions/yfm/Video/actions.ts
packages/editor/src/extensions/yfm/Video/index.ts
Make the circular dependency CI script type-aware by disabling dpdm’s transform option so type-only imports are included in the graph.
  • Remove the transform: true option from scripts/check-circular-deps.js when calling parseDependencyTree, relying on dpdm defaults so import type edges are preserved
  • Drop the unused import/no-extraneous-dependencies eslint-disable since dpdm is clearly a dev dependency and script no longer needs that override
packages/editor/scripts/check-circular-deps.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The move of Extension* types from core/types/extension into ExtensionBuilder is good for breaking cycles, but core/index.ts no longer re-exports these types at all; if Extension/ExtensionAuto/etc. were part of the public core API, consider re-exporting them from core/index.ts via ExtensionBuilder to avoid a breaking change.
  • With MarkdownSerializerDynamicModifier now co-located in MarkdownSerializer.ts, the public entry point core/markdown/MarkdownSerializerDynamicModifier.ts is reduced to a thin re-export; it might be worth adding a brief comment there documenting that this file is a compatibility facade so future refactors don’t accidentally remove it and break deep imports.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The move of `Extension*` types from `core/types/extension` into `ExtensionBuilder` is good for breaking cycles, but `core/index.ts` no longer re-exports these types at all; if `Extension`/`ExtensionAuto`/etc. were part of the public `core` API, consider re-exporting them from `core/index.ts` via `ExtensionBuilder` to avoid a breaking change.
- With `MarkdownSerializerDynamicModifier` now co-located in `MarkdownSerializer.ts`, the public entry point `core/markdown/MarkdownSerializerDynamicModifier.ts` is reduced to a thin re-export; it might be worth adding a brief comment there documenting that this file is a compatibility facade so future refactors don’t accidentally remove it and break deep imports.

## Individual Comments

### Comment 1
<location path="packages/editor/src/core/ExtensionBuilder.ts" line_range="299" />
<code_context>
     return map;
 }

+export type Extension = (builder: ExtensionBuilder) => void;
+export type ExtensionWithOptions<T> = (builder: ExtensionBuilder, options: T) => void;
+export type ExtensionAuto<T = void> = T extends void ? Extension : ExtensionWithOptions<T>;
</code_context>
<issue_to_address>
**issue (complexity):** Consider moving this new block of extension-related type definitions into a dedicated, colocated type module so the main builder file stays focused on behavior.

You can reduce cognitive load here by factoring the new type block back into a dedicated type module colocated with `ExtensionBuilder`, while keeping all behavior unchanged.

For example, extract the type definitions into a separate file:

```ts
// ExtensionTypes.ts (or ./types/extension.ts if that’s the established pattern)
import type MarkdownIt from 'markdown-it';
import OrderedMap from 'orderedmap';
import type {MarkSpec, NodeSpec, Schema} from 'prosemirror-model';

import type {ActionSpec, ActionStorage} from './types/actions';
import type {MarkViewConstructor, NodeViewConstructor} from './types/node-views';
import type {Parser, ParserToken} from './types/parser';
import type {Serializer, SerializerMarkToken, SerializerNodeToken} from './types/serializer';
import type {ExtensionBuilder} from './ExtensionBuilder';

export type Extension = (builder: ExtensionBuilder) => void;
export type ExtensionWithOptions<T> = (builder: ExtensionBuilder, options: T) => void;
export type ExtensionAuto<T = void> = T extends void ? Extension : ExtensionWithOptions<T>;

export type ExtensionNodeSpec = {
    spec: NodeSpec;
    view?: (deps: ExtensionDeps) => NodeViewConstructor;
    fromMd: {
        tokenName?: string;
        tokenSpec: ParserToken;
    };
    toMd: SerializerNodeToken;
};

export type ExtensionMarkSpec = {
    spec: MarkSpec;
    view?: (deps: ExtensionDeps) => MarkViewConstructor;
    fromMd: {
        tokenName?: string;
        tokenSpec: ParserToken;
    };
    toMd: SerializerMarkToken;
};

export type ExtensionDeps = {
    readonly schema: Schema;
    readonly textParser: Parser;
    readonly markupParser: Parser;
    readonly serializer: Serializer;
    readonly actions: ActionStorage;
};

export type ExtensionSpec = {
    configureMd(md: MarkdownIt, parserType: 'text' | 'markup'): MarkdownIt;
    nodes(): OrderedMap<ExtensionNodeSpec>;
    marks(): OrderedMap<ExtensionMarkSpec>;
    plugins(deps: ExtensionDeps): Plugin[];
    actions(deps: ExtensionDeps): Record<string, ActionSpec>;
};
```

Then keep `ExtensionBuilder` focused on behavior:

```ts
// ExtensionBuilder.ts
import type {Extension, ExtensionAuto, ExtensionSpec, ExtensionDeps} from './ExtensionTypes';
// ...rest of imports

export class ExtensionBuilder {
    // existing implementation unchanged
}
```

This preserves all the new capabilities (`Schema`, `Parser`, `Serializer`, `ActionStorage`, view constructors, etc.) while:

- Making `ExtensionBuilder.ts` easier to scan (behavior first, types elsewhere).
- Centralizing the extension type system in one reusable module, reducing the risk of drift with `core/types/extension.ts`.
- Keeping the large type-level surface area in a dedicated place that can evolve independently of the builder logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/editor/src/core/ExtensionBuilder.ts
Comment thread packages/editor/src/extensions/yfm/YfmNote/YfmNoteSpecs/schema.ts Outdated
Comment thread packages/editor/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts Outdated
Comment thread packages/editor/src/extensions/markdown/Deflist/DeflistSpecs/schema.ts Outdated
Comment thread packages/editor/src/extensions/markdown/Deflist/DeflistSpecs/index.ts Outdated
Comment thread packages/editor/src/extensions/behavior/WidgetDecoration/actions.ts Outdated
Comment thread packages/editor/src/core/index.ts
- Break type-only circular dependencies by introducing leaf type modules
  and co-locating option types with their schema implementations.
- WidgetDecoration: extract `Meta` to leaf module `meta.ts` (parameterized
  by descriptor type to avoid the type-only cycle); restore `satisfies Meta`
  in `actions.ts` and `WidgetDescriptor#applyTo`. Public `Meta` in
  `types.ts` is exposed as `Meta = MetaGeneric<WidgetDescriptor>`.
- core/index.ts: keep explicit re-export of `Extension*` types from
  `./ExtensionBuilder` for an explicit public API surface.
- YfmNoteSpecs/DeflistSpecs: move schema-level option type into
  `schema.ts` as `*SchemaOptions`; keep public `*SpecsOptions` in
  `index.ts` as an alias of the schema options (matches the existing
  Tabs/Table/Cut/Checkbox convention).
- Configure check-circular-deps to surface type-only cycles
  (no longer transformed away).

Made-with: Cursor
@makhnatkin makhnatkin force-pushed the fix/types-circular-deps branch from b036c83 to 619f5a5 Compare April 23, 2026 17:18
@makhnatkin makhnatkin requested a review from d3m1d0v April 23, 2026 17:22
@makhnatkin makhnatkin merged commit e0a96fa into main Apr 24, 2026
7 checks passed
@makhnatkin makhnatkin deleted the fix/types-circular-deps branch April 24, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants