Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 19 additions & 1 deletion .github/actions/aggregate-report/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,26 @@ outputs:
runs:
using: 'composite'
steps:
- name: Download artifacts
- name: Download head coverage artifact
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: test-report
path: test-report

- name: Download base coverage artifact
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: test-report-base
path: test-report-base

- name: Download mutation report artifact
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: mutation-report
path: mutation-report

- name: Find current PR's number
uses: jwalton/gh-find-current-pr@v1
Expand Down
16 changes: 16 additions & 0 deletions .github/actions/base-coverage/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,41 @@ runs:
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0

- name: Check package exists on base ref
id: check
shell: bash
run: |
if [ -f "${{ inputs.working-directory }}/package.json" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "Package not present on base ref (new package), skipping base coverage"
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

- name: Setup Node
if: steps.check.outputs.exists == 'true'
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc

- name: Setup environment
if: steps.check.outputs.exists == 'true'
uses: ./.github/actions/setup

- name: Build the package
if: steps.check.outputs.exists == 'true'
shell: bash
run: yarn workspace ${{ inputs.package-name }} run build
working-directory: ${{ inputs.working-directory }}

- name: Run coverage
if: steps.check.outputs.exists == 'true'
shell: bash
run: yarn workspace ${{ inputs.package-name }} test:coverage
working-directory: ${{ inputs.working-directory }}

- name: Upload base coverage artifact
if: steps.check.outputs.exists == 'true'
uses: actions/upload-artifact@v7
with:
name: test-report-base
Expand Down
9 changes: 5 additions & 4 deletions .github/scripts/compare-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ function findCoverageJson(dir) {
if (!dir) return null;
const report = path.join(dir, 'coverage', 'coverage-summary.json');


if (fs.existsSync(report)) {
try {
return JSON.parse(fs.readFileSync(report, 'utf8'));
} catch (report) {
console.error(report);
} catch (err) {
console.error(err);
}
}

Expand Down Expand Up @@ -54,6 +53,8 @@ export function processReports(pkg, headDir, baseDir) {

const categories = compute(headCov, baseCov);

const headPct = categories.branches.head != null ? `${categories.branches.head}%` : 'N/A';

let delta = categories.branches.delta;

if (delta < 0) {
Expand All @@ -65,7 +66,7 @@ export function processReports(pkg, headDir, baseDir) {
}

// | Package | Branches coverage | Delta |
return `| ${pkg} | ${categories.branches.head}% | ${delta} |`;
return `| ${pkg} | ${headPct} | ${delta} |`;
}


2 changes: 1 addition & 1 deletion .github/workflows/collaboration-manager.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
with:
package-name: '@editorjs/collaboration-manager'
working-directory: './packages/collaboration-manager'
include-mutations: false
include-mutations: true
secrets:
stryker_dashboard_api_key: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

20 changes: 20 additions & 0 deletions .github/workflows/model-types.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Model-types check
on:
pull_request:
merge_group:

permissions:
contents: read
pull-requests: write
issues: write
actions: read

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
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
28 changes: 20 additions & 8 deletions packages/collaboration-manager/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import { type JestConfigWithTsJest, createDefaultEsmPreset } from 'ts-jest';
import type { JestConfigWithTsJest } from 'ts-jest';

export default {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.test.json',
},
},
coverageReporters: ['lcov', 'json-summary', 'text-summary'],
testMatch: ['<rootDir>/src/**/*.spec.ts'],
modulePathIgnorePatterns: ['<rootDir>/.*/__mocks__', '<rootDir>/.*/mocks'],
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'^codex-tooltip$': '<rootDir>/test/mocks/codex-tooltip.ts',
},
coverageReporters: ['lcov', 'json-summary', 'text-summary'],
transform: {
...createDefaultEsmPreset().transform,
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
tsconfig: '<rootDir>/tsconfig.test.json',
},
],
'^.+\\.jsx?$': [
'babel-jest',
{
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
],
},
],
},
transformIgnorePatterns: [
'node_modules/(?!@editorjs)',
],
} as JestConfigWithTsJest;
Loading
Loading