From 2e96dc703c284fe3c4241fe7079aea06c4e100dc Mon Sep 17 00:00:00 2001 From: draedful Date: Mon, 6 Apr 2026 13:17:38 +0300 Subject: [PATCH 01/13] feat(types): resolve graph colors/constants and document strict migration - Add TResolvedGraphColors and merge helpers; wire Graph, Layer, events, API - Accept RecursivePartial for color/constant updates; fix useGraph view config - Export TResolvedGraphColors; add TS strict mode analysis doc Made-with: Cursor --- .../typescript-strict-mode-analysis.md | 193 ++++++++++++++++++ src/api/PublicGraphApi.ts | 9 +- src/graph.ts | 23 ++- src/graphConfig.ts | 33 +++ src/graphEvents.ts | 4 +- src/index.ts | 2 +- src/react-components/GraphCanvas.tsx | 4 +- src/react-components/hooks/useGraph.ts | 8 +- src/services/Layer.ts | 4 +- 9 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 docs/analysis/typescript-strict-mode-analysis.md diff --git a/docs/analysis/typescript-strict-mode-analysis.md b/docs/analysis/typescript-strict-mode-analysis.md new file mode 100644 index 00000000..978ada5c --- /dev/null +++ b/docs/analysis/typescript-strict-mode-analysis.md @@ -0,0 +1,193 @@ +# Анализ ошибок TypeScript при включении `strict` + +Документ фиксирует объём и характер ошибок компилятора при проверке проекта в строгом режиме **без изменений в коде**. Дата снятия метрик: **2026-04-06**. + +## Методология + +- **Команда:** `npx tsc --noEmit -p tsconfig.json --strict` +- **Базовый конфиг:** `tsconfig.json` (в репозитории **не** задан `"strict": true`; проверка выполнялась с флагом CLI). +- **Что фактически включает `--strict`** (по `tsc --showConfig`): +`strictNullChecks`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `strictBuiltinIteratorReturn`, `alwaysStrict`, `noImplicitAny`, `noImplicitThis`, `useUnknownInCatchVariables`. + +## Сводные цифры + + +| Метрика | Значение | +| ----------------------------------------------------------------- | -------- | +| **Всего диагностик** (`error TS…`) | **662** | +| **Ядро библиотеки** (`src/`, без `stories/` и без `*.test.ts(x)`) | **415** | +| **Stories** (`src/stories/`) | **216** | +| **Тесты** (`*.test.ts(x)` и пр. в `src/`) | **31** | + + +Stories дают **~33%** всех ошибок; сфокусированная миграция «только пакет» может отдельно оценивать подмножество **~415** ошибок в коде библиотеки. + +## Распределение по коду ошибки (все 662) + + +| Код | Кол-во | Кратко (что означает) | +| ------- | ------ | --------------------------------------------------------------------------------------------- | +| TS18048 | 157 | Значение **возможно `undefined`** (`?.` / проверки не сужают тип). | +| TS2322 | 125 | Тип **не присваивается** ожидаемому (часто вместе с `undefined` / `null`). | +| TS2345 | 123 | **Аргумент** не подходит под параметр (в т.ч. контравариантность `Event` vs `MouseEvent`). | +| TS2532 | 57 | Объект **возможно `undefined`** (доступ к свойству/методу). | +| TS7006 | 43 | Параметр с **неявным `any`** (`noImplicitAny`). | +| TS2564 | 32 | Поле класса **без инициализации** и без definite assignment (`strictPropertyInitialization`). | +| TS18047 | 30 | Значение **возможно `null`**. | +| TS7053 | 27 | Индексация `**string` по объекту без индексной сигнатуры** → неявный `any`. | +| TS2783 | 15 | В объектном литерале **одно и то же поле задано дважды** (часто в stories). | +| TS7005 | 10 | Переменная с **неявным `any`** (вывод типа не удался). | +| TS2538 | 9 | В качестве индекса используется `**undefined` / небезопасный ключ**. | +| TS7034 | 6 | Переменная с **неявным `any[]`** (нужна явная аннотация или инициализация). | +| TS2769 | 5 | **Нет подходящей перегрузки** (часто `addEventListener` / узкие типы событий). | +| TS2531 | 5 | Объект **возможно `null`**. | +| TS2540 | 4 | Присваивание в **только для чтения** свойство. | +| TS2722 | 2 | Вызов **возможно `undefined`**. | +| TS2488 | 2 | Ожидается итерируемое, тип **не итерируем**. | +| TS2454 | 2 | Переменная используется **до присваивания**. | +| TS2365 | 2 | Оператор **не применим** к данным типам. | +| TS2344 | 2 | Аргумент generic **не удовлетворяет ограничению**. | +| TS2339 | 2 | Свойство **отсутствует** (в т.ч. на типе `never` — логическая ошибка в ветвлении типов). | +| TS7019 | 1 | Rest-параметр с **неявным `any[]`**. | +| TS7016 | 1 | **Нет деклараций** для модуля (`style-observer` → неявный `any`). | + + +## Смысловая группировка (по типу проблемы) + +Ниже — не взаимоисключающие «корзины» (одна строка кода может порождать несколько кодов); группы удобны для планирования работ. + +### 1. Null / undefined и строгие присваивания (~70% всех диагностик по смыслу) + +- **Коды:** TS18048, TS18047, TS2532, TS2531, большая доля TS2322 и TS2345. +- **Проявления:** опциональные поля в store/API, `Array`/`Map` без гарантии элемента, цвета/строки для Canvas (`string | undefined` vs `string | CanvasGradient | CanvasPattern`), цепочки после `find`. +- **Зоны в ядре:** рендер блоков/связей, anchors, `MultipointConnection`, layers, `PublicGraphApi`. +- **Цвета и константы графа:** часть ошибок из‑за того, что `TGraphColors` описывает частичный ввод, а не смерженное состояние; см. **«План: нормализация `colors` и `constants`»**. + +### 2. Строгая инициализация полей классов + +- **Код:** TS2564 (32). +- **Примеры файлов:** `Block.ts`, `Blocks.ts`, `BaseConnection.ts`, `BlockConnection.ts` и др. +- **Суть:** поля, заполняемые позже в lifecycle, без `!`, без инициализатора и без присваивания в конструкторе. + +### 3. Неявный `any` и небезопасная индексация + +- **Коды:** TS7006, TS7005, TS7034, TS7019, TS7053. +- **Суть:** параметры без типов, «пустой» `{}` как тип состояния с динамическими ключами, индекс `string` по объектам без сигнатуры. + +### 4. События DOM и перегрузки + +- **Коды:** TS2345 (сужение `Event` → `MouseEvent`), TS2769 (`addEventListener` / union слушателей). +- **Зоны:** `Block`, `GraphComponent`, слои с HTML. + +### 5. Логика типов `never` / исчерпывающесть + +- **Коды:** TS2339 на `never`, TS2345 с `never` (например, накопление в массив без явного типа элементов). +- **Указатель на рефакторинг типов**, а не только «подправить null». + +### 6. Внешние модули без типов + +- **TS7016:** `style-observer` — нужен ambient-модуль, декларации в репозитории или типы от пакета. + +### 7. Stories: дубликаты ключей в объектах + +- **TS2783:** повторяющееся поле `config` (и аналоги) в литералах — в основном **stories**, правки обычно тривиальны. + +## Где ошибок больше всего (ядро, без stories и без `*.test`) + +Файлы с наибольшим числом диагностик (ориентир для приоритизации): + + +| Файл (путь от `src/`) | Ошибок (порядок) | +| --------------------------------------------------------------------- | ---------------- | +| `components/canvas/connections/MultipointConnection.ts` | 31 | +| `services/Layer.ts` | 21 | +| `plugins/devtools/DevToolsLayer.ts` | 20 | +| `lib/CoreComponent.ts` | 20 | +| `components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts` | 20 | +| `utils/functions/text.ts` | 19 | +| `components/canvas/blocks/Block.ts` | 19 | +| `components/canvas/layers/newBlockLayer/NewBlockLayer.ts` | 18 | +| `components/canvas/layers/belowLayer/PointerGrid.ts` | 12 | +| `components/canvas/layers/connectionLayer/ConnectionLayer.ts` | 11 | +| `components/canvas/connections/BatchPath2D/index.tsx` | 11 | + + +## Категории важности и порядок работ + +### Высокий приоритет (лучше начать с этого) + +1. **Нормализация `colors` / `constants` (см. отдельный план ниже):** единый слой «разрешённых» типов после `merge` с дефолтами — снимает большой пласт TS18048 / TS2322 / TS2532 вокруг `context.colors`, `GraphCanvas`, событий `colors-changed` и опционального `viewConfiguration.constants` в хуках. +2. **Базовые абстракции:** `CoreComponent`, `Layer` — от них зависят слои и компоненты; исправления здесь уменьшают каскад ошибок downstream. +3. **Публичный контракт:** `graph.ts`, `PublicGraphApi`, типы store (`Block`, `ConnectionState`, `ConnectionList`) — влияют на потребителей и стабильность API. +4. **Внешняя типизация:** TS7016 для `style-observer` — маленький объём, но важно для «чистого» strict без `skipLibCheck`-костылей на уровне приложения. +5. **Ошибки на `never` / массивы как `never`:** указывают на неверный вывод типов; лучше исправить рано, иначе появятся скрытые `as` и дублирование логики. + +### Средний приоритет + +1. **Рендер связей и canvas:** `MultipointConnection`, `BatchPath2D`, `BaseConnection` — много null/Canvas-строк; локальные, но объёмные (часть уйдёт после нормализации цветов, останутся геометрия и прочие `undefined`). +2. **React-обвязка:** после нормализации — точечные правки в `useGraph` (передача только определённых partial в `setConstants`), конвертеры elk — видимость для пользователей React-энтрипоинта. +3. **Сервисы:** `HitTest`, `DragService`, `SelectionService` (в тестах тоже есть ошибки) — взаимодействие и hit-test. + +### Низкий приоритет / можно отложить + +1. **Stories (~216 ошибок):** часто демо-код, дубликаты полей в конфигах, меньше риска для публикуемых типов. Имеет смысл чинить **после** или **параллельно** с ядром, либо вынести в отдельный `tsconfig` с более мягкими правилами на переходный период. +2. **Тесты (~31):** моки, частичные объекты, «удобные» утилиты — обычно последними, когда типы ядра стабилизированы. +3. **DevTools plugin:** много ошибок в одном файле, но не входит в минимальный runtime графа для потребителей — можно после стабилизации основных слоёв. + +## План: нормализация `colors` и `constants` + +Цель — совпадение **типов** с **инвариантом рантайма**: после `lodash/merge` с `initGraphColors` / `initGraphConstants` в графе всегда есть полное дерево значений, но сейчас оно типизировано как частичное (`TGraphColors` с опциональными секциями и `Partial<…>` внутри), из‑за чего strict ругается на `colors.block`, `colors.anchor`, поля для `CanvasRenderingContext2D.fillStyle` и т.д. + +### Шаг 1. Ввести типы «ввод» vs «разрешено» + + +| Роль | Назначение | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Ввод (config API)** | Оставить текущую идею: `RecursivePartial` для `setColors`, опциональные `colors` / `constants` в `viewConfiguration` и `TGraphConfig`. При необходимости явно назвать алиас, например `TGraphColorsPatch`. | +| **Разрешённое состояние** | Новый тип, например `TResolvedGraphColors`: все секции (`block`, `canvas`, `anchor`, …) **обязательны**, внутри каждой секции все поля из `TBlockColors`, `TCanvasColors`, … — **обязательные `string`** (или как в исходных интерфейсах). Построить через утилиту уровня «deep required» для фиксированного набора ключей верхнего уровня, чтобы не тащить лишние опции из patch-типа. | +| **Constants** | Аналогично: `TResolvedGraphConstants` как полностью разрешённый объект после merge с `initGraphConstants`; для API — по-прежнему `RecursivePartial` там, где допускается частичное обновление. | + + +Публичный экспорт: разрешённые типы нужны потребителям, которые читают `graph.graphColors` или подписываются на события — им должно быть ясно, что строки цветов не `undefined`. + +### Шаг 2. Функции нормализации + +- `**resolveGraphColors(patch: TGraphColors): TResolvedGraphColors`** — внутри `merge({}, initGraphColors, patch)` (или эквивалент) и возврат с типом `TResolvedGraphColors`. Одна точка правды рядом с `initGraphColors` в `graphConfig.ts` (или рядом с `Graph`). +- `**resolveGraphConstants(patch: RecursivePartial): TResolvedGraphConstants**` — то же для констант и `initGraphConstants`. + +Так рантайм не меняется концептуально: нормализация уже делается в `setColors` / `setConstants`; функции либо **оборачивают** существующий merge, либо заменяют его с явным приведением результата к разрешённому типу (после проверки, что дефолты покрывают все ключи). + +### Шаг 3. Подключить типы в состоянии графа + +- Тип значения `**$graphColors`** и геттера `**graphColors**`: `TResolvedGraphColors`. +- Тип значения `**$graphConstants**` и геттера `**graphConstants**`: `TResolvedGraphConstants`. +- Сигнатуры `**setColors` / `setConstants**`: принимают partial / recursive partial; присваивают результату нормализованное значение. +- `**LayerContext.colors**`, payload события `**colors-changed**`, аргументы колбэков в `**GraphCanvas**`: использовать разрешённый тип, чтобы не размножать `?.` и `??` по canvas-компонентам. + +### Шаг 4. Сопутствующие правки + +- `**useGraph`:** в ветках обновления view configuration передавать в сеттеры только определённые значения (`if (viewConfig.constants) graph.setConstants(viewConfig.constants)`), не пробрасывать `undefined` туда, где тип ожидает объект. +- **Экспорты пакета:** при смене типа `graphColors` проверить changelog / мажорную версию, если сигнатуры публичных типов меняются с «всё опционально» на «всё задано». + +### Ожидаемый эффект + +- Уходит дублирование дефолтов через `??` в каждом месте чтения цвета. +- Секция **«Null / undefined»** в плане для цветов/констант смещается с «точечные guard’ы» на «один слой нормализации + строгие типы состояния». + +--- + +## Практические рекомендации по миграции (без правок кода в этом шаге) + +- **Поэтапное включение в `tsconfig`:** сначала отдельные флаги (`strictNullChecks`, затем `strictPropertyInitialization`, затем полный `strict`) снижают шок от объёма; либо отдельный `tsconfig.strict.json` для CI. +- **Разделение проектов:** `src/`** под strict, stories — отдельная компиляция или отложенный этап, чтобы не блокировать библиотеку. +- **Colors / constants:** следовать разделу **«План: нормализация `colors` и `constants`»** до массового добавления `?.`/`??` в рендер. +- **Паттерны исправлений (ожидаемые):** сужение типов guard’ами, `??`/`!` только там, где есть инвариант, инициализаторы полей или `declare`/assign в `init`, явные типы вместо неявного `any`, для событий — слушатели с `Event` + `instanceof` или обёртки, для динамических ключей — `Record<…>` / mapped types вместо `{}`. + +## Артефакт проверки + +Полный лог компилятора можно воспроизвести командой выше; при необходимости сохранить вывод: +`npx tsc --noEmit -p tsconfig.json --strict 2>&1 | tee typescript-strict-errors.log`. + +--- + +*Документ подготовлен как входная точка для плана включения strict mode; код репозитория на момент анализа не изменялся.* \ No newline at end of file diff --git a/src/api/PublicGraphApi.ts b/src/api/PublicGraphApi.ts index 3ee7cb34..31b1f37f 100644 --- a/src/api/PublicGraphApi.ts +++ b/src/api/PublicGraphApi.ts @@ -3,7 +3,7 @@ import { batch } from "@preact/signals-core"; import { GraphComponent } from "../components/canvas/GraphComponent"; import { TBlock } from "../components/canvas/blocks/Block"; import { Graph } from "../graph"; -import { TGraphColors, TGraphConstants } from "../graphConfig"; +import { TGraphColors, TGraphConstants, TResolvedGraphColors } from "../graphConfig"; import { ESelectionStrategy } from "../services/selection/types"; import { TBlockId } from "../store/block/Block"; import { selectBlockById } from "../store/block/selectors"; @@ -11,6 +11,7 @@ import { TConnection, TConnectionId } from "../store/connection/ConnectionState" import { selectConnectionById } from "../store/connection/selectors"; import { TGraphSettingsConfig } from "../store/settings"; import { getBlocksRect, getElementsRect, startAnimation } from "../utils/functions"; +import type { RecursivePartial } from "../utils/types/helpers"; import { TRect } from "../utils/types/shapes"; export type ZoomConfig = { @@ -143,11 +144,11 @@ export class PublicGraphApi { }); } - public getGraphColors(): TGraphColors { + public getGraphColors(): TResolvedGraphColors { return this.graph.graphColors; } - public updateGraphColors(colors: TGraphColors) { + public updateGraphColors(colors: RecursivePartial): void { this.graph.setColors(colors); } @@ -155,7 +156,7 @@ export class PublicGraphApi { return this.graph.graphConstants; } - public updateGraphConstants(constants: TGraphConstants) { + public updateGraphConstants(constants: RecursivePartial): void { this.graph.setConstants(constants); } diff --git a/src/graph.ts b/src/graph.ts index 1fb92510..2e21d001 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -1,5 +1,4 @@ import { batch, signal } from "@preact/signals-core"; -import merge from "lodash/merge"; import { PublicGraphApi, ZoomConfig } from "./api/PublicGraphApi"; import { GraphComponent } from "./components/canvas/GraphComponent"; @@ -8,7 +7,13 @@ import { BelowLayer } from "./components/canvas/layers/belowLayer/BelowLayer"; import { CursorLayer, CursorLayerCursorTypes } from "./components/canvas/layers/cursorLayer"; import { GraphLayer } from "./components/canvas/layers/graphLayer/GraphLayer"; import { SelectionLayer } from "./components/canvas/layers/selectionLayer/SelectionLayer"; -import { TGraphColors, TGraphConstants, initGraphColors, initGraphConstants } from "./graphConfig"; +import { + createInitialResolvedGraphColors, + initGraphConstants, + mergeResolvedGraphColors, + mergeResolvedGraphConstants, +} from "./graphConfig"; +import type { TGraphColors, TGraphConstants, TResolvedGraphColors } from "./graphConfig"; import { GraphEvent, GraphEventParams, GraphEventsDefinitions, isGraphEvent } from "./graphEvents"; import { scheduler } from "./lib/Scheduler"; import { HitTest } from "./services/HitTest"; @@ -94,7 +99,7 @@ export class Graph { return this.$graphColors.value; } - public $graphColors = signal(initGraphColors); + public $graphColors = signal(createInitialResolvedGraphColors()); public get graphConstants() { return this.$graphConstants.value; @@ -123,8 +128,8 @@ export class Graph { constructor( config: TGraphConfig, rootEl?: HTMLDivElement, - graphColors?: TGraphColors, - graphConstants?: TGraphConstants + graphColors?: RecursivePartial, + graphConstants?: RecursivePartial ) { this.belowLayer = this.addLayer(BelowLayer, {}); this.graphLayer = this.addLayer(GraphLayer, {}); @@ -160,13 +165,13 @@ export class Graph { return this.graphLayer; } - public setColors(colors: RecursivePartial) { - this.$graphColors.value = merge({}, this.$graphColors.value, colors); + public setColors(colors: RecursivePartial): void { + this.$graphColors.value = mergeResolvedGraphColors(this.$graphColors.value, colors); this.emit("colors-changed", { colors: this.graphColors }); } - public setConstants(constants: RecursivePartial) { - this.$graphConstants.value = merge({}, this.$graphConstants.value, constants); + public setConstants(constants: RecursivePartial): void { + this.$graphConstants.value = mergeResolvedGraphConstants(this.$graphConstants.value, constants); this.emit("constants-changed", { constants: this.graphConstants }); } diff --git a/src/graphConfig.ts b/src/graphConfig.ts index 86b47c5d..48ac1d3f 100644 --- a/src/graphConfig.ts +++ b/src/graphConfig.ts @@ -1,6 +1,9 @@ +import merge from "lodash/merge"; + import { GraphComponent } from "./components/canvas/GraphComponent"; import { Block } from "./components/canvas/blocks/Block"; import { ESelectionStrategy } from "./services/selection"; +import type { RecursivePartial } from "./utils/types/helpers"; export type { TResolveWheelDevice } from "./utils/functions/isTrackpadDetector"; export { defaultResolveWheelDevice, EWheelDeviceKind } from "./utils/functions/isTrackpadDetector"; @@ -52,6 +55,18 @@ export type TCanvasColors = { border: string; }; +/** + * Colors after merge with defaults (`initGraphColors`). All sections and keys are defined. + */ +export type TResolvedGraphColors = { + canvas: TCanvasColors; + block: TBlockColors; + anchor: TAnchorColors; + connection: TConnectionColors; + connectionLabel: TConnectionLabelColors; + selection: TSelectionColors; +}; + export type TMouseWheelBehavior = "zoom" | "scroll"; export const initGraphColors: TGraphColors = { @@ -89,6 +104,17 @@ export const initGraphColors: TGraphColors = { }, }; +export function createInitialResolvedGraphColors(): TResolvedGraphColors { + return merge({}, initGraphColors) as TResolvedGraphColors; +} + +export function mergeResolvedGraphColors( + current: TResolvedGraphColors, + patch: RecursivePartial +): TResolvedGraphColors { + return merge({}, current, patch) as TResolvedGraphColors; +} + /** * Constructor type for any class that extends GraphComponent */ @@ -303,3 +329,10 @@ export const initGraphConstants: TGraphConstants = { PADDING: 10, }, }; + +export function mergeResolvedGraphConstants( + current: TGraphConstants, + patch: RecursivePartial +): TGraphConstants { + return merge({}, current, patch) as TGraphConstants; +} diff --git a/src/graphEvents.ts b/src/graphEvents.ts index c68b641f..ed7dc645 100644 --- a/src/graphEvents.ts +++ b/src/graphEvents.ts @@ -1,6 +1,6 @@ import { EventedComponent } from "./components/canvas/EventedComponent/EventedComponent"; import { GraphState } from "./graph"; -import { TGraphColors, TGraphConstants } from "./graphConfig"; +import { TGraphConstants, TResolvedGraphColors } from "./graphConfig"; import { TCameraState } from "./services/camera/CameraService"; import { TSelectionDiff, TSelectionEntityId } from "./services/selection/types"; @@ -40,7 +40,7 @@ export type UnwrapBaseGraphEventsDetail< export interface GraphEventsDefinitions extends BaseGraphEventDefinition { "camera-change": (event: CustomEvent) => void; "constants-changed": (event: CustomEvent<{ constants: TGraphConstants }>) => void; - "colors-changed": (event: CustomEvent<{ colors: TGraphColors }>) => void; + "colors-changed": (event: CustomEvent<{ colors: TResolvedGraphColors }>) => void; "state-change": (event: CustomEvent<{ state: GraphState }>) => void; } const graphMouseEvents = ["mousedown", "click", "dblclick", "mouseenter", "mousemove", "mouseleave"]; diff --git a/src/index.ts b/src/index.ts index 429c1501..874ce1c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export { Block as CanvasBlock, type TBlock } from "./components/canvas/blocks/Bl export { GraphComponent } from "./components/canvas/GraphComponent"; export * from "./components/canvas/connections"; export * from "./graph"; -export type { TGraphColors, TGraphConstants, TMouseWheelBehavior } from "./graphConfig"; +export type { TGraphColors, TGraphConstants, TMouseWheelBehavior, TResolvedGraphColors } from "./graphConfig"; export type { TResolveWheelDevice } from "./utils/functions/isTrackpadDetector"; export { defaultResolveWheelDevice, EWheelDeviceKind } from "./utils/functions/isTrackpadDetector"; export { type UnwrapGraphEventsDetail, type SelectionEvent } from "./graphEvents"; diff --git a/src/react-components/GraphCanvas.tsx b/src/react-components/GraphCanvas.tsx index 62ebe7a6..52f98cff 100644 --- a/src/react-components/GraphCanvas.tsx +++ b/src/react-components/GraphCanvas.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useLayoutEffect, useRef } from "react"; -import { TGraphColors } from ".."; +import { TResolvedGraphColors } from ".."; import { Graph } from "../graph"; import { setCssProps } from "../utils/functions/cssProp"; @@ -55,7 +55,7 @@ export function GraphCanvas({ useGraphEvents(graph, cbs); - const setColors = useFn((colors: TGraphColors) => { + const setColors = useFn((colors: TResolvedGraphColors) => { setCssProps(containerRef.current, { "--graph-block-bg": colors.block.background, "--graph-block-border": colors.block.border, diff --git a/src/react-components/hooks/useGraph.ts b/src/react-components/hooks/useGraph.ts index b914dbad..3bb12ae2 100644 --- a/src/react-components/hooks/useGraph.ts +++ b/src/react-components/hooks/useGraph.ts @@ -43,11 +43,11 @@ export function useGraph(config: HookGraphParams) { }, [graph]); const setViewConfiguration = useFn((viewConfig: HookGraphParams["viewConfiguration"]) => { - if (viewConfig.colors) { - graph.setColors(config.viewConfiguration.colors); + if (viewConfig?.colors) { + graph.setColors(viewConfig.colors); } - if (viewConfig.constants) { - graph.setConstants(config.viewConfiguration.constants); + if (viewConfig?.constants) { + graph.setConstants(viewConfig.constants); } }); diff --git a/src/services/Layer.ts b/src/services/Layer.ts index 9f7f0e5d..9edfe5b6 100644 --- a/src/services/Layer.ts +++ b/src/services/Layer.ts @@ -1,5 +1,5 @@ import { Graph } from "../graph"; -import { TGraphColors, TGraphConstants } from "../graphConfig"; +import { TGraphConstants, TResolvedGraphColors } from "../graphConfig"; import { GraphEventsDefinitions } from "../graphEvents"; import { CoreComponent } from "../lib"; import { Component, TComponentState } from "../lib/Component"; @@ -49,7 +49,7 @@ export type LayerContext = { graph: Graph; camera: ICamera; constants: TGraphConstants; - colors: TGraphColors; + colors: TResolvedGraphColors; graphCanvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; layer: Layer; From b2a54570f5b4b33c35a2cfff1cf4c35d2baf0221 Mon Sep 17 00:00:00 2001 From: draedful Date: Mon, 6 Apr 2026 13:36:50 +0300 Subject: [PATCH 02/13] fix(react): import TResolvedGraphColors from graphConfig, not barrel Made-with: Cursor --- src/react-components/GraphCanvas.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/GraphCanvas.tsx b/src/react-components/GraphCanvas.tsx index 52f98cff..d72a40f6 100644 --- a/src/react-components/GraphCanvas.tsx +++ b/src/react-components/GraphCanvas.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useRef } from "react"; -import { TResolvedGraphColors } from ".."; import { Graph } from "../graph"; +import type { TResolvedGraphColors } from "../graphConfig"; import { setCssProps } from "../utils/functions/cssProp"; import { TBlockListProps } from "./BlocksList"; From 2c123a9b4251f995a55eb6df10e9ef783385d093 Mon Sep 17 00:00:00 2001 From: draedful Date: Mon, 6 Apr 2026 13:45:16 +0300 Subject: [PATCH 03/13] fix: strict TS hardening for blocks/connections; dev log instead of throw - Block, Blocks, BaseConnection, BlockConnection: types and guards - Anchor optional store binding; PublicGraphApi updateConnection no-op if missing - Add logDev for NODE_ENV !== production Made-with: Cursor --- src/api/PublicGraphApi.ts | 7 +++- src/components/canvas/anchors/index.ts | 34 +++++++++++------- src/components/canvas/blocks/Block.ts | 36 ++++++++++++------- src/components/canvas/blocks/Blocks.ts | 18 ++++++---- .../canvas/connections/BaseConnection.ts | 14 ++++++-- .../canvas/connections/BlockConnection.ts | 7 ++-- src/utils/devLog.ts | 9 +++++ 7 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 src/utils/devLog.ts diff --git a/src/api/PublicGraphApi.ts b/src/api/PublicGraphApi.ts index 31b1f37f..a473eefe 100644 --- a/src/api/PublicGraphApi.ts +++ b/src/api/PublicGraphApi.ts @@ -10,6 +10,7 @@ import { selectBlockById } from "../store/block/selectors"; import { TConnection, TConnectionId } from "../store/connection/ConnectionState"; import { selectConnectionById } from "../store/connection/selectors"; import { TGraphSettingsConfig } from "../store/settings"; +import { logDev } from "../utils/devLog"; import { getBlocksRect, getElementsRect, startAnimation } from "../utils/functions"; import type { RecursivePartial } from "../utils/types/helpers"; import { TRect } from "../utils/types/shapes"; @@ -226,8 +227,12 @@ export class PublicGraphApi { }); } - public updateConnection(id: TConnectionId, connection: Partial) { + public updateConnection(id: TConnectionId, connection: Partial): void { const connectionStore = selectConnectionById(this.graph, id); + if (!connectionStore) { + logDev(`updateConnection: connection not found: ${String(id)}`); + return; + } connectionStore.updateConnection(connection); } diff --git a/src/components/canvas/anchors/index.ts b/src/components/canvas/anchors/index.ts index d7b1bb27..33773c2b 100644 --- a/src/components/canvas/anchors/index.ts +++ b/src/components/canvas/anchors/index.ts @@ -4,6 +4,7 @@ import { AnchorState, EAnchorType } from "../../../store/anchor/Anchor"; import { TBlockId } from "../../../store/block/Block"; import { selectBlockAnchor } from "../../../store/block/selectors"; import { PortState } from "../../../store/connection/port/Port"; +import { logDev } from "../../../utils/devLog"; import { GraphComponent, TGraphComponentProps } from "../GraphComponent"; import { GraphLayer } from "../layers/graphLayer/GraphLayer"; @@ -44,7 +45,7 @@ export class Anchor extends GraphComponen return this.__comp.parent.zIndex + 1; } - public connectedState: AnchorState; + public connectedState?: AnchorState; private shift = 0; @@ -52,8 +53,13 @@ export class Anchor extends GraphComponen super(props, parent); this.state = { size: props.size, raised: false, selected: false }; - this.connectedState = selectBlockAnchor(this.context.graph, props.blockId, props.id); - this.connectedState.setViewComponent(this); + const anchorState = selectBlockAnchor(this.context.graph, props.blockId, props.id); + if (!anchorState) { + logDev(`Anchor not found: block "${String(props.blockId)}", anchor "${String(props.id)}"`); + } else { + this.connectedState = anchorState; + this.connectedState.setViewComponent(this); + } this.addEventListener("click", this); this.addEventListener("mouseenter", this); @@ -91,9 +97,11 @@ export class Anchor extends GraphComponen protected willMount(): void { this.props.port.setOwner(this); - this.subscribeSignal(this.connectedState.$selected, (selected) => { - this.setState({ selected }); - }); + if (this.connectedState) { + this.subscribeSignal(this.connectedState.$selected, (selected) => { + this.setState({ selected }); + }); + } this.subscribeSignal(this.props.port.$point, this.onPositionChanged); this.computeShift(this.state, this.props); this.onPositionChanged(); @@ -133,8 +141,8 @@ export class Anchor extends GraphComponen return this.props.port.getPoint(); } - public toggleSelected() { - this.connectedState.setSelection(!this.state.selected); + public toggleSelected(): void { + this.connectedState?.setSelection(!this.state.selected); } /** @@ -153,15 +161,15 @@ export class Anchor extends GraphComponen } public override handleDragStart(context: DragContext): void { - this.connectedState.block.getViewComponent()?.handleDragStart(context); + this.connectedState?.block.getViewComponent()?.handleDragStart(context); } public override handleDrag(diff: DragDiff, context: DragContext): void { - this.connectedState.block.getViewComponent()?.handleDrag(diff, context); + this.connectedState?.block.getViewComponent()?.handleDrag(diff, context); } public override handleDragEnd(context: DragContext): void { - this.connectedState.block.getViewComponent()?.handleDragEnd(context); + this.connectedState?.block.getViewComponent()?.handleDragEnd(context); } protected isVisible() { @@ -169,9 +177,9 @@ export class Anchor extends GraphComponen return params ? this.context.camera.isRectVisible(...params) : true; } - protected unmount() { + protected unmount(): void { this.props.port.removeOwner(); - this.connectedState.unsetViewComponent(); + this.connectedState?.unsetViewComponent(); super.unmount(); } diff --git a/src/components/canvas/blocks/Block.ts b/src/components/canvas/blocks/Block.ts index 9f5026ca..d557ad33 100644 --- a/src/components/canvas/blocks/Block.ts +++ b/src/components/canvas/blocks/Block.ts @@ -12,6 +12,7 @@ import { BlockState, IS_BLOCK_TYPE, TBlockId } from "../../../store/block/Block" import { selectBlockById } from "../../../store/block/selectors"; import { PortState } from "../../../store/connection/port/Port"; import { createAnchorPortId, createBlockPointPortId } from "../../../store/connection/port/utils"; +import { logDev } from "../../../utils/devLog"; import { isAllowDrag, isMetaKeyEvent } from "../../../utils/functions"; import { clamp } from "../../../utils/functions/clamp"; import { TMeasureTextOptions } from "../../../utils/functions/text"; @@ -104,7 +105,7 @@ export class Block; + public connectedState!: BlockState; private connectedStateUnsubscribers: (() => void)[] = []; @@ -112,13 +113,9 @@ export class Block