Публикация расширений

Публикация расширений в kepler.gl строится вокруг понимания архитектуры библиотеки и контрактов, через которые она допускает внешнее расширение функциональности. Экосистема Kepler.gl предполагает, что расширения не просто подключаются как вспомогательные модули, а интегрируются в поток данных, состояние карты и UI-слой React-приложения.

Kepler.gl построен на связке React + Redux, где карта управляется через единое состояние (mapState, visState, uiState). Расширения в этой модели могут вмешиваться в три ключевые точки:

  • UI-слой — добавление новых компонентов интерфейса
  • State layer — расширение Redux-редьюсеров и действий
  • Layer system — добавление новых типов визуализаций

Ключевая идея публикации расширений заключается в том, чтобы не ломать ядро, а внедряться через заранее определённые точки расширения.

Формат расширения

Типичное расширение Kepler.gl представляет собой JavaScript/TypeScript пакет, который экспортирует:

  • компоненты (React)
  • редьюсеры (Redux)
  • actions (Redux action creators)
  • middleware (опционально)
  • конфигурации для инъекции в KeplerGl container

Структура пакета обычно выглядит следующим образом:

kepler-gl-extension/
 ├── src/
 │   ├── components/
 │   ├── reducers/
 │   ├── actions/
 │   ├── layer-types/
 │   └── index.ts
 ├── package.json
 ├── rollup.config.js
 └── tsconfig.json

Интеграция с состоянием Kepler.gl

Расширение часто начинается с добавления собственного редьюсера:

export const customReducer = (state = initialState, action) => {
    switch (action.type) {
        case 'CUSTOM_ACTION':
            return {
                ...state,
                value: action.payload
            };
        default:
            return state;
    }
};

После этого редьюсер должен быть подключён к корневому редьюсеру Kepler.gl через combineReducers.

Добавление действий (actions)

Действия позволяют расширению взаимодействовать с состоянием карты:

export const setCustomValue = (value) => ({
    type: 'CUSTOM_ACTION',
    payload: value
});

Важно учитывать, что действия Kepler.gl должны быть совместимы с его middleware pipeline, иначе возможны конфликты при асинхронных обновлениях состояния карты.

Расширение UI-компонентов

UI-расширения обычно внедряются через контейнер KeplerGl:

import KeplerGl from 'kepler.gl';

export default function MapWrapper() {
    return (
        <KeplerGl
            id="map"
            width={800}
            height={600}
        />
    );
}

Расширение может добавлять собственные панели через HOC (Higher Order Components):

const withCustomPanel = (Panel) => (props) => {
    return (
        <>
            <Panel {...props} />
            <div className="custom-panel">
                Custom content
            </div>
        </>
    );
};

Подключение кастомных слоёв

Одной из наиболее сложных частей является добавление новых типов слоёв (Layer Types). Kepler.gl использует декларативную модель описания слоя.

Пример базовой структуры слоя:

import { Layer } from 'kepler.gl/layers';

class CustomLayer extends Layer {
    constructor(props) {
        super(props);
        this.type = 'customLayer';
    }

    renderLayer(opts) {
        const { data, objectColor } = opts;

        return {
            vertices: data,
            color: objectColor
        };
    }
}

После создания слоя его необходимо зарегистрировать в системе:

export const customLayerType = {
    type: 'customLayer',
    Layer: CustomLayer
};

Сборка и подготовка пакета

Публикация расширения невозможна без корректной сборки. Обычно используется Rollup или Vite в библиотечном режиме.

Пример Rollup-конфига:

export default {
    input: 'src/index.ts',
    output: [
        {
            file: 'dist/index.cjs.js',
            format: 'cjs'
        },
        {
            file: 'dist/index.esm.js',
            format: 'esm'
        }
    ],
    external: [
        'react',
        'react-dom',
        'kepler.gl'
    ]
};

Критически важно объявлять Kepler.gl как peerDependency, чтобы избежать дублирования зависимостей:

{
  "peerDependencies": {
    "kepler.gl": ">=2.0.0",
    "react": ">=17.0.0"
  }
}

Версионирование расширений

Расширения Kepler.gl должны строго соблюдать semantic versioning:

  • MAJOR — несовместимые изменения API расширения
  • MINOR — добавление функциональности без нарушения обратной совместимости
  • PATCH — исправления и оптимизации

При публикации в npm важно учитывать совместимость с конкретной версией Kepler.gl, так как внутренняя структура store может меняться.

Публикация в npm

Процесс публикации включает подготовку пакета и его отправку в registry:

npm login
npm publish --access public

Перед публикацией необходимо убедиться, что:

  • собран dist-пакет
  • отсутствуют dev-зависимости в финальном бандле
  • корректно настроены entry points (main, module, types)

Пример package.json:

{
  "name": "kepler-gl-custom-extension",
  "version": "1.0.0",
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts"
}

Подключение опубликованного расширения

После публикации расширение подключается в приложении:

import { customReducer } from 'kepler-gl-custom-extension';

const reducers = combineReducers({
    keplerGl: keplerGlReducer,
    custom: customReducer
});

Далее расширение становится частью общего состояния приложения и может взаимодействовать с картой через Redux.

Совместимость и конфликты

Основные проблемы при публикации расширений возникают из-за:

  • несовпадения версий Kepler.gl
  • конфликтов Redux namespace
  • дублирования React экземпляра
  • некорректной изоляции side effects

Особенно критична проблема двойного React, которая приводит к ошибкам хуков:

Invalid hook call. Hooks can only be called inside of the body of a function component

Для устранения используется строгая настройка peerDependencies и внешних зависимостей в сборщике.

Тестирование расширений

Расширения должны тестироваться в двух плоскостях:

  • unit-тесты редьюсеров и actions
  • интеграционные тесты с Kepler.gl контейнером

Пример теста редьюсера:

test('custom reducer sets value', () => {
    const state = customReducer(undefined, {
        type: 'CUSTOM_ACTION',
        payload: 10
    });

    expect(state.value).toBe(10);
});

Документирование и примеры использования

Публикуемое расширение должно включать:

  • README с описанием API
  • примеры интеграции с KeplerGl container
  • минимальный рабочий пример (sandbox или Storybook)

Структура документации обычно отражает реальные сценарии использования:

  • подключение reducer
  • добавление UI компонента
  • регистрация слоя
  • обработка событий карты

Расширяемость как контракт

Публикация расширений в Kepler.gl фактически означает работу с контрактной архитектурой. Любое расширение должно:

  • не модифицировать ядро напрямую
  • использовать публичные API Kepler.gl
  • изолировать side effects
  • сохранять предсказуемость Redux-цепочки

Такая модель обеспечивает возможность масштабирования экосистемы без разрушения базовой архитектуры карты и визуализации пространственных данных.