Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/model-types.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Model-types check
on:
pull_request:
merge_group:

jobs:
package-check:
uses: ./.github/workflows/package-check.yml
with:
package-name: '@editorjs/model-types'
working-directory: './packages/model-types'
include-mutations: false
secrets:
stryker_dashboard_api_key: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ jest-report.json

# ENV
**/.env
/.claude/
/--help/
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
yarn constraints
Comment thread
gohabereg marked this conversation as resolved.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ A model-driven, collaboration-ready Editor.js engine split into focused packages

| Package | Description |
|---|---|
| [`@editorjs/sdk`](packages/sdk) | Shared contracts — interfaces, base event classes, `EventBus` |
| [`@editorjs/model`](packages/model) | In-memory document model (`EditorJSModel`, `BlockNode`, `TextNode`, caret management) |
| [`@editorjs/model-types`](packages/model-types) | Shared low-level types and base event classes used internally by `model` and `sdk` only — not intended for direct use by other packages or tools |
| [`@editorjs/sdk`](packages/sdk) | Shared contracts — interfaces, base event classes, `EventBus`. The package tools and plugins should depend on |
| [`@editorjs/model`](packages/model) | In-memory document model (`EditorJSModel`, `BlockNode`, `TextNode`, caret management). Internal engine used by `core`/`ot-server` — tools and plugins should use `@editorjs/sdk` instead |
| [`@editorjs/dom-adapters`](packages/dom-adapters) | Binds model nodes to DOM inputs (`DOMBlockToolAdapter`, `CaretAdapter`, `FormattingAdapter`) |
| [`@editorjs/collaboration-manager`](packages/collaboration-manager) | Operational transformation, batching, undo/redo, OT WebSocket client |
| [`@editorjs/core`](packages/core) | Orchestrator — IoC container, plugin/tool lifecycle, `EditorAPI` |
Expand Down
11 changes: 7 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Architecture Overview

The editor is split into eight packages in a layered dependency direction.
The editor is split into nine packages in a layered dependency direction.

| Package | Role |
|---|---|
| `@editorjs/model-types` | Shared low-level types, nominal brands, and base event classes (`Index`, `BaseDocumentEvent`, the model event classes, `EventBus`). No runtime dependencies of its own. |
| `@editorjs/sdk` | Shared contracts — interfaces, base event classes, `EventBus` |
| `@editorjs/model` | In-memory document model (`EditorJSModel`) |
| `@editorjs/dom-adapters` | Binds model nodes to DOM inputs; default adapter implementation |
Expand All @@ -15,12 +16,14 @@ The editor is split into eight packages in a layered dependency direction.

## Dependency rules

- `model-types` is the foundation layer: it has no dependency on `model` or `sdk`, and **only `model` and `sdk` may depend on it directly**. Every other package (`dom-adapters`, `collaboration-manager`, `ui`, and tools/plugins in general) that needs `Index`, event classes, or other model-types primitives should get them re-exported through `@editorjs/sdk` instead of depending on `@editorjs/model-types` directly. This exists so `model` and `sdk` can share the same `Index`/event/nominal-type definitions without `sdk` depending on the full `model` engine (and vice versa) — see `packages/model-types/src/index.ts` for the exact re-exported surface.
- `sdk` is the contract layer all other packages depend on.
- `model` is the engine implementation that backs `EditorJSModel`. It is consumed directly by `core` (the orchestrator) and `ot-server` (server-side document state), but **tools and plugins should never import `@editorjs/model` directly** — they should depend on `@editorjs/sdk`'s contracts (`BlockTool`, `InlineTool`, `Index`, event types, etc.) instead. `sdk` re-exports everything from `model` that a tool/plugin author legitimately needs, so `model` itself isn't part of the stable, tool-facing API surface and is free to change its internals.
- `core` wires runtime dependencies; it should be the only orchestrator.
- `model` does not depend on DOM concerns.
- `dom-adapters` and `collaboration-manager` observe/apply model changes through public APIs and events.
- `ui` depends on `sdk` and `model`; it is registered as an `EditorjsPlugin` via `core.use()`.
- `ot-server` depends on `collaboration-manager` (for `Operation` / message types) and `model`; it runs server-side only.
- `dom-adapters` and `collaboration-manager` observe/apply model changes through public APIs and events, and depend only on `sdk` (not `model` or `model-types`).
- `ui` depends on `sdk`; it is registered as an `EditorjsPlugin` via `core.use()`.
- `ot-server` depends on `collaboration-manager` (for `Operation` / message types), `model`, and `sdk`; it runs server-side only.

## Runtime ownership

Expand Down
31 changes: 24 additions & 7 deletions docs/diagrams/architecture-overview.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ classDiagram
direction LR


%% Layer 0 — Contracts (@editorjs/sdk)
namespace sdk {

%% Layer 0 — Foundation (@editorjs/model-types)
%% Internal-only: depended on by `model` and `sdk` directly; every other
%% package (and all tools/plugins) should get these via `@editorjs/sdk` instead.
namespace modelTypes {
class EventBus {
<<EventTarget>>
+addEventListener(type, callback)
+removeEventListener(type, callback)
+dispatchEvent(event)
}

class Index {
+blockIndex: number
+dataKey: DataKey
+textRange: TextRange
+serialize(): string
+parse(serialized): Index
}
}

%% Layer 1 — Contracts (@editorjs/sdk)
namespace sdk {

class BlockToolAdapter {
<<interface>>
+attachInput(keyRaw, input: HTMLElement)
Expand Down Expand Up @@ -52,7 +65,7 @@ classDiagram
}
}

%% Layer 1 — Data model (@editorjs/model)
%% Layer 2 — Data model (@editorjs/model)
namespace model {
class EditorJSModel {
+serialized: EditorDocumentSerialized
Expand All @@ -69,14 +82,14 @@ classDiagram
}
}

%% Layer 2a — DOM binding (@editorjs/dom-adapters)
%% Layer 3a — DOM binding (@editorjs/dom-adapters)
namespace domAdapters {
class DOMAdapters {
+createBlockToolAdapter(blockIndex, toolName): BlockToolAdapter
}
}

%% Layer 2b — Collaboration plugin (@editorjs/collaboration-manager)
%% Layer 3b — Collaboration plugin (@editorjs/collaboration-manager)
namespace collaborationManager {
class CollaborationManager {
<<EditorjsPlugin>>
Expand All @@ -87,7 +100,7 @@ classDiagram
}
}

%% Layer 3 — Orchestrator (@editorjs/core)
%% Layer 4 — Orchestrator (@editorjs/core)
namespace core {
class Core {
+constructor(config)
Expand All @@ -105,6 +118,10 @@ classDiagram
EditorJSModel --|> EventBus : extends
Core *-- EventBus : creates & holds

%% Foundation usage — model-types is depended on by model and sdk only
EditorJSModel ..> Index : uses (caret/selection addressing)
BlockToolAdapter ..> Index : uses (caret/selection addressing)

%% Core — owns & wires
Core *-- EditorJSModel
Core *-- UndoRedoManager : creates (listens on EventBus)
Expand Down
13 changes: 11 additions & 2 deletions docs/diagrams/events-catalog.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ classDiagram
direction LR

%% ── Base classes ─────────────────────────────────
%% BaseDocumentEvent is defined in @editorjs/model-types, like the
%% concrete events below it.
class BaseDocumentEvent {
<<abstract CustomEvent>>
detail.index: Index
Expand Down Expand Up @@ -44,8 +46,10 @@ classDiagram
<<adapter EventBus, per-block>>
}

%% ── @editorjs/model ──────────────────────────────
namespace model {
%% ── @editorjs/model-types ────────────────────────
%% Defined here, internal-only; re-exported by both `model` (for
%% EditorJSModel to dispatch) and `sdk` (for tools/plugins to consume).
namespace modelTypes {
class BlockAddedEvent {
detail.data: BlockNodeSerialized
}
Expand Down Expand Up @@ -79,6 +83,10 @@ classDiagram
class TuneModifiedEvent {
detail.data.value: T
detail.data.previous: T
}
class PropertyModifiedEvent {
detail.data.value: T
detail.data.previous: T
}
class CaretManagerCaretUpdatedEvent {
<<CustomEvent~CaretSerialized~>>
Expand Down Expand Up @@ -156,6 +164,7 @@ classDiagram
DataNodeRemovedEvent --|> BaseDocumentEvent
ValueModifiedEvent --|> BaseDocumentEvent
TuneModifiedEvent --|> BaseDocumentEvent
PropertyModifiedEvent --|> BaseDocumentEvent

BlockAddedCoreEvent --|> CoreEventBase
BlockRemovedCoreEvent --|> CoreEventBase
Expand Down
4 changes: 4 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Plugins & Tools

## Package boundary

Tools and plugins should only depend on `@editorjs/sdk` — never on `@editorjs/model` or `@editorjs/model-types` directly. `sdk` re-exports every type a tool/plugin author needs (`Index`, event classes, `BlockTool`/`InlineTool`/`BlockTune` contracts, etc.); `model` is the engine implementation that `core` and `ot-server` orchestrate, and `model-types` is an internal foundation shared only by `model` and `sdk`. Neither is part of the stable, tool-facing API.

## Registration

`core.use(...)` registers UI components/plugins by static `type` (values from `ToolType` for tools, `PluginType.Adapter` for adapters, and `PluginType.Plugin` for general plugins).
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"build": "yarn workspaces foreach -At run build",
"test": "yarn workspaces foreach -A run test",
"lint": "yarn workspaces foreach -A run lint",
"lint:fix": "yarn workspaces foreach -A run lint --fix"
"lint:fix": "yarn workspaces foreach -A run lint --fix",
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.1.7"
}
}
2 changes: 1 addition & 1 deletion packages/collaboration-manager/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default [
* For test files allow dev dependencies imports
*/
'n/no-unpublished-import': ['error', {
allowModules: ['@jest/globals'],
allowModules: ['@jest/globals', '@editorjs/model'],
}],
/**
* Used for ws mock in test files
Expand Down
6 changes: 3 additions & 3 deletions packages/collaboration-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@
"clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo"
},
"dependencies": {
"@editorjs/model": "workspace:^",
"@editorjs/sdk": "workspace:^"
},
"devDependencies": {
"@editorjs/model": "workspace:^",
"@jest/globals": "^29.7.0",
"@stryker-mutator/core": "^7.0.2",
"@stryker-mutator/jest-runner": "^7.0.2",
"@stryker-mutator/typescript-checker": "^7.0.2",
"@types/eslint": "^8",
"@types/jest": "^29.5.12",
"@types/jest": "^30.0.0",
"eslint": "^8.38.0",
"eslint-config-codex": "^2.0.2",
"eslint-plugin-import": "^2.29.0",
"jest": "^29.7.0",
"jest": "^30.4.2",
"stryker-cli": "^1.0.2",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/collaboration-manager/src/BatchedOperation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Index } from '@editorjs/model';
import { createDataKey, IndexBuilder, type TextRange } from '@editorjs/model';
import type { Index } from '@editorjs/sdk';
import { createDataKey, IndexBuilder, type TextRange } from '@editorjs/sdk';
import { BatchedOperation } from './BatchedOperation.js';
import type { SerializedOperation } from './Operation.js';
import { Operation, OperationType } from './Operation.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import { createDataKey, EventType, IndexBuilder } from '@editorjs/model';
import { createDataKey, EventType, IndexBuilder } from '@editorjs/sdk';
import { EditorJSModel } from '@editorjs/model';
import { CoreEventType, type CoreConfig } from '@editorjs/sdk';
import { beforeAll, jest } from '@jest/globals';
Expand Down
2 changes: 1 addition & 1 deletion packages/collaboration-manager/src/CollaborationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
TextAddedEvent,
TextFormattedEvent, TextRemovedEvent,
TextUnformattedEvent
} from '@editorjs/model';
} from '@editorjs/sdk';
import type {
UndoCoreEvent,
EditorAPI,
Expand Down
4 changes: 2 additions & 2 deletions packages/collaboration-manager/src/Operation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import type { BlockNodeSerialized, DataKey, DocumentIndex } from '@editorjs/model';
import { IndexBuilder } from '@editorjs/model';
import type { BlockNodeSerialized, DataKey, DocumentIndex } from '@editorjs/sdk';
import { IndexBuilder } from '@editorjs/sdk';
import { describe } from '@jest/globals';
import { type InsertOrDeleteOperationData, type ModifyOperationData, Operation, OperationType } from './Operation.js';

Expand Down
4 changes: 2 additions & 2 deletions packages/collaboration-manager/src/Operation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TextRange } from '@editorjs/model';
import { IndexBuilder, type Index, type BlockNodeSerialized } from '@editorjs/model';
import type { TextRange } from '@editorjs/sdk';
import { IndexBuilder, type Index, type BlockNodeSerialized } from '@editorjs/sdk';
import { OperationsTransformer } from './OperationsTransformer.js';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DocumentId, Index } from '@editorjs/model';
import { createDataKey, IndexBuilder } from '@editorjs/model';
import type { DocumentId, Index } from '@editorjs/sdk';
import { createDataKey, IndexBuilder } from '@editorjs/sdk';
import { Operation, OperationType } from './Operation.js';
import { OperationsTransformer } from './OperationsTransformer.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IndexBuilder } from '@editorjs/model';
import { IndexBuilder } from '@editorjs/sdk';
import { Operation, OperationType } from './Operation.js';
import { getRangesIntersectionType, RangeIntersectionType } from './utils/getRangesIntersectionType.js';

Expand Down
4 changes: 2 additions & 2 deletions packages/collaboration-manager/src/UndoRedoManager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DocumentId } from '@editorjs/model';
import { createDataKey, IndexBuilder } from '@editorjs/model';
import type { DocumentId } from '@editorjs/sdk';
import { createDataKey, IndexBuilder } from '@editorjs/sdk';
import { describe } from '@jest/globals';
import { jest } from '@jest/globals';
import { Operation, OperationType } from './Operation.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/collaboration-manager/src/client/Message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DocumentId, EditorDocumentSerialized } from '@editorjs/model';
import type { DocumentId, EditorDocumentSerialized } from '@editorjs/sdk';
import type { MessageType } from './MessageType.js';
import type { SerializedOperation } from '../Operation.js';

Expand Down
4 changes: 2 additions & 2 deletions packages/collaboration-manager/src/client/OTClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import type { DocumentId, EditorDocumentSerialized } from '@editorjs/model';
import { createDataKey, IndexBuilder } from '@editorjs/model';
import type { DocumentId, EditorDocumentSerialized } from '@editorjs/sdk';
import { createDataKey, IndexBuilder } from '@editorjs/sdk';
import { beforeEach, afterEach, jest, describe, it, expect } from '@jest/globals';
import { OTClient } from './OTClient.js';
import { Operation, OperationType } from '../Operation.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/collaboration-manager/src/client/OTClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EditorDocumentSerialized } from '@editorjs/model';
import type { EditorDocumentSerialized } from '@editorjs/sdk';
import { Operation, OperationType, type SerializedOperation } from '../Operation.js';
import type { HandshakeMessage, HandshakePayload, Message, OperationMessage } from './Message.js';
import { MessageType } from './MessageType.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TextRange } from '@editorjs/model';
import type { TextRange } from '@editorjs/sdk';

/**
* Represents the type of intersection between two text ranges.
Expand Down
19 changes: 13 additions & 6 deletions packages/collaboration-manager/test/mocks/createManager.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import type { EditorDocumentSerialized, ModelEvents } from '@editorjs/model';
import { EventType } from '@editorjs/model';
import type { EditorJSModel } from '@editorjs/model';
import { EventBus } from '@editorjs/sdk';
import type { EditorDocumentSerialized, ModelEvents, Index } from '@editorjs/sdk';
import { EventBus, EventType } from '@editorjs/sdk';
import type { CoreConfigValidated, DocumentAPI, EditorAPI, InsertRemoveDataParams, ModifyDataParams, BlocksAPI } from '@editorjs/sdk';
import { CollaborationManager } from '../../src/CollaborationManager.js';

interface EditorModel {
serialized: EditorDocumentSerialized;
addEventListener(type: string, callback: (event: ModelEvents) => void): void;
removeEventListener(type: string, callback: (event: ModelEvents) => void): void;
insertData(userId: string | number | undefined, index: Index, data: unknown): void;
removeData(userId: string | number | undefined, index: Index, data: unknown): void;
modifyData(userId: string | number | undefined, index: Index, data: unknown): void;
}

/**
* Creates a mock DocumentAPI backed by a real EditorJSModel instance
* @param model - the EditorJS model to back the mock API with
*/
function createMockDocumentAPI(model: EditorJSModel): DocumentAPI {
function createMockDocumentAPI(model: EditorModel): DocumentAPI {
return {
get data(): EditorDocumentSerialized {
return model.serialized;
Expand Down Expand Up @@ -37,7 +44,7 @@ function createMockDocumentAPI(model: EditorJSModel): DocumentAPI {
* @param model - the EditorJS model instance
* @returns an object containing the manager and the eventBus used
*/
export function createManager(config: CoreConfigValidated, model: EditorJSModel): {
export function createManager(config: CoreConfigValidated, model: EditorModel): {
manager: CollaborationManager;
eventBus: EventBus;
} {
Expand Down
3 changes: 0 additions & 3 deletions packages/collaboration-manager/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
"src/**/*.spec.ts"
],
"references": [
{
"path": "../model/tsconfig.build.json"
},
{
"path": "../sdk/tsconfig.build.json"
}
Expand Down
5 changes: 5 additions & 0 deletions packages/collaboration-manager/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,10 @@
"node_modules/**/*",
"dist/**/*",
"**/*.spec.ts"
],
"references": [
{
"path": "../sdk/tsconfig.build.json"
}
]
}
Loading
Loading