npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@emailmaker/ui-kit

v1.1.84

Published

UI Kit for Emailmaker

Readme

UI Kit

Набор UI-компонентов и темы для plugin-system.

Источник публичного README для npm: этот каталог (PACKAGE_CONFIG['ui-kit'].docsPath). Сборка: build/ui-kit/README.md.

Публичная документация:

📦 Система плагинов для emailmaker

Библиотека предоставляет расширяемую архитектуру для создания плагинов для визуального редактора писем emailmaker. Плагины позволяют:

  • расширять функциональность редактора;
  • разрабатывать собственные UI-компоненты с использованием экспортированных версий React и Ant Design
  • взаимодействовать с кодом письма в iframe;
  • отображать модальные окна на основе Ant Design в редакторе;

Разработка осуществляется с полной поддержкой TypeScript, что обеспечивает строгость типов, модульность и предсказуемость.


⚙️ Требования

  • Современный браузер с поддержкой ES2015 (и выше)
  • Node.js >= 20 (рекомендуется LTS; CLI-шаблон использует tsx и Vite/Webpack 5)
  • React и Ant Design в плагине подключаются через PluginDev (alias/externals на runtime хоста из @emailmaker/emailmaker/runtime/*), а не отдельным npm-пакетом runtime

📘 Термины

  • sandbox — часть плагина, работающая внутри iframe, для взаимодействия с содержимым письма.
  • app — часть плагина, встроенная в редактор. Отвечает за интерфейс.
  • pluginRegistry — механизм регистрации плагинов.
  • externals — способ подключения внешних библиотек в webpack.

🔰 Быстрый старт

👉 Рекомендуемый способ — CLI (@emailmaker/cli): согласованная структура, Vite/Webpack, app + sandbox, dev/release host.

npx @emailmaker/cli my-plugin --kind plugin --preset advanced --bundler vite --target npm-package --output-dir ./my-plugin
cd my-plugin
npm install
npm run start

Сгенерированный проект включает:

  • Webpack / Vite (по выбору)
  • React + Ant Design
  • TypeScript
  • Структуру проекта, описанную ниже

Проверка release-сборки без ручного деплоя:

npm run build
npm run start:release

Альтернатива: пример из репозитория

Готовый example можно клонировать для ознакомления; для новых плагинов предпочтительнее CLI-шаблон.

git clone https://github.com/emailmaker
cd simple_plugin
npm install
npm run start

🛠 Установка

📦 Основные пакеты

npm install @emailmaker/emailmaker @emailmaker/extensions-app @emailmaker/extensions-react @emailmaker/extensions-sandbox

| Пакет | Назначение | Что экспортирует | |-------|------------|------------------| | @emailmaker/emailmaker | Основной пакет | init(), prefetch(), типы IPlugin, Instance; runtime React/Ant Design — subpath @emailmaker/emailmaker/runtime/* (отдельный npm не нужен) | | @emailmaker/extensions-app | API для app-плагинов | pluginRegistry, DomComponentRegistry, ElementsApi, SettingsPanelApi, ModalApi | | @emailmaker/extensions-react | React API для плагинов | ComponentRegistry, createComponentIdentifier() | | @emailmaker/extensions-sandbox | API для sandbox-плагинов | App, MessageService, SyncService |

@emailmaker/ui-kit ставится отдельно (devDependency в CLI-шаблоне), если плагин использует готовые UI-компоненты платформы.

React и Ant Design в коде плагина импортируются как react / antd. PluginDev в dev/release подставляет runtime хоста через alias/externals — см. раздел «Сборка плагина».

Импорты

// React и Antd — обычные импорты
import React from 'react';
import { Button } from 'antd';

// UI-компоненты платформы (опционально)
import { ColorPicker } from '@emailmaker/ui-kit';

// Core API плагинов
import { pluginRegistry, ElementsApi } from '@emailmaker/extensions-app';

// React-пакет для плагинов
import { createComponentIdentifier } from '@emailmaker/extensions-app';
import { ComponentRegistry } from '@emailmaker/extensions-react';

// Типы из основного пакета
import type { IPlugin, Instance } from '@emailmaker/emailmaker';

Что обычно ставить

  • если у вас обычный React-плагин, ставьте все пакеты из команды выше
  • если используете UI Kit, добавьте ещё @emailmaker/ui-kit
  • если пишете только sandbox-часть, достаточно main + @emailmaker/extensions-sandbox

Для React-плагинов пакет @emailmaker/extensions-react нужен по умолчанию.


🔧 Сборка плагина

Используйте PluginDev для настройки бандлера. Он делает две вещи:

  1. alias — настраивает React и Ant Design для работы в плагине
  2. externals — выносит общие зависимости из бандла
npm install @emailmaker/emailmaker

Таргеты для автора плагина

| Таргет | Что получится | Внутри использует | |--------|----------------|-------------------| | npm-пакет | ESM bundle для package-based интеграции | 'esm' | | Встраиваемый bundle | UMD/script-подключение в host | 'globals' | | Advanced | Webpack-only async globals compatibility | 'async-globals' / 'legacy-async-globals' |

PluginDev({ externals: false }) используется только для local dev host и не считается release target.

Типичный проект: serve + build

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

| Режим | Entry | Externals | Основной плагин | |-------|-------|-----------|-----------------| | serve | src/dev.ts | false | Да (статика, iframe) | | build | src/index.ts | зависит от выбранного target | Нет |

// src/dev.ts — тестовая страница с основным приложением
import '@emailmaker/emailmaker';
import { MyPlugin } from './index';

emailmaker.init({ plugins: [MyPlugin] });

Vite:

import VitePlugin from '@emailmaker/emailmaker/vite';
import PluginDev from '@emailmaker/emailmaker/vite/pluginDev';

export default defineConfig(({ command }) => ({
  plugins: [
    command === 'serve' && VitePlugin(),
    PluginDev({ externals: command === 'build' ? 'esm' : false }),
  ],
  build: {
    lib: { entry: 'src/index.ts', formats: ['es'] },
  },
}));

Webpack:

const WebpackPlugin = require('@emailmaker/emailmaker/webpack');
const PluginDev = require('@emailmaker/emailmaker/webpack/pluginDev');
const isDev = process.env.NODE_ENV === 'development';

module.exports = {
  entry: isDev ? './src/dev.ts' : './src/index.ts',
  plugins: [
    isDev && new WebpackPlugin(),
    new PluginDev({ externals: isDev ? false : 'esm' }),
  ].filter(Boolean),
};

VitePlugin / WebpackPlugin — основной плагин для хост-приложений. В serve обслуживает iframe, шрифты и статику. В build — копирует их в output.

Опции PluginDev

| Опция | По умолчанию | Значения | |-------|-------------|----------| | externals | 'esm' | 'esm', 'globals', 'async-globals' (Webpack), false | | alias | mode-aware | true / false |

Для большинства внешних плагинов не выбирайте externals вручную — используйте CLI target:

  • npm-пакет -> esm
  • встраиваемый bundle -> globals
  • advanced -> только если host уже требует async/globals контракт Webpack

Для встраиваемый bundle и advanced рекомендуемый runtime-контракт — descriptor вида { type: 'umd', url, name, resolve: 'registry' }, где name совпадает с ключом из PluginTypeMap.

Ручная настройка (без PluginDev)

Нужна только для продвинутой интеграции или собственного бандлерного пайплайна. Основной сценарий для внешнего плагина — отдельный пакет с PluginDev.

  • resolve.alias: react → @emailmaker/emailmaker/runtime/react (и т.д.) для esm / dev; локальные shared internals идут через compat facades runtime; UI-kit (@emailmaker/ui-kit) → @emailmaker/emailmaker/ui-kit (singleton bytes на main, не runtime/@scope/ui-kit)
  • externals: @emailmaker/emailmaker/runtime/core, runtime npm-модули; ui-kit external → @emailmaker/emailmaker/ui-kit (глобальный ключ UiKit в runtimeExports, см. манифест)
  • Список модулей: @emailmaker/emailmaker/runtime/plugin-dev-manifest.json → runtimeModules

Если release bundle неожиданно стал толстым или target встраиваемый bundle не загружается, начните с раздела Troubleshooting.

Типы для плагина всё равно собираются отдельно: CLI-шаблон запускает build:types и кладёт dist/public-types.d.ts рядом с JS bundle, поэтому TypeScript support сохраняется и для browser/advanced targets.

tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

Что писать в коде

В коде плагина пишите обычные импорты:

import React from 'react';
import { Button } from 'antd';

А React API для плагинов берите из @emailmaker/extensions-react:

import { createComponentIdentifier } from '@emailmaker/extensions-app';
import { ComponentRegistry } from '@emailmaker/extensions-react';

Обычно достаточно использовать готовый шаблон CLI и не настраивать alias/external вручную.


📂 PublicPath

publicPath используется для указания базового URL для загрузки второстепенных скриптов и статики плагина. Более подробно прочитать про publicPath можно в секции publicPath документации webpack

Внутри плагина:

export interface MyPluginOptions {
  publicPath?: string;
}

class MyPlugin implements IPlugin {
  constructor(
    private editor: Instance,
    private options: MyPluginOptions = {}
  ) {}

  init() {
    if (this.options.publicPath) {
      const sandboxApi = this.editor.use('SandboxScriptApi');
      sandboxApi.registerSandboxScript(this.options.publicPath + 'sandbox.js');
    }
  }
}

Подключение плагина с publicPath:

emailmaker.init({
  plugins: [
    ['MyPlugin', { publicPath: 'https://cdn.example.com/my-plugin/' }]
  ]
});

📁 Структура проекта

CLI-шаблон (типичный layout):

my-plugin/
├── src/
│   ├── app/              # UI-часть, взаимодействие с редактором
│   │   └── MyPlugin.tsx
│   ├── sandbox/          # Скрипт для DOM письма (отдельная сборка)
│   │   └── index.ts
│   ├── dev.tsx           # Локальный debug host (редактор + плагин)
│   └── public-types.d.ts # Типы плагина для npm / augmentation
├── config/               # Vite/Webpack: dev host, release, sandbox
├── scripts/              # dev-runner, build-runner, release-runner, build-types
├── dist/                 # Сборка app-плагина (+ public-types.d.ts)
└── package.json
  • src/app — регистрация плагина, панели, работа с API редактора. Отсюда подключается sandbox:
const sandboxApi = this.editor.use('SandboxScriptApi');
sandboxApi.registerSandboxScript(this.options.publicPath + 'sandbox.js');
  • src/sandbox — код в iframe; собирается отдельным entry (build:sandbox), без React.
  • src/dev.tsx — локальный запуск emailmaker с плагином (npm run start).
  • src/public-types.d.ts — PluginTypeMap, Config, опции; после npm run build:types копируется в dist/public-types.d.ts.

Общие идентификаторы событий app ↔ sandbox держите в src/interfaces/ (или аналогичном shared-модуле внутри src/).


🧩 Жизненный цикл плагина

Плагин может быть реализован в виде класса или фабрики. В конструкторе класс получает экземпляр редактора emailmaker и должен соответствовать интерфейсу:

export interface IPlugin {
  required?(): void;
  init?(): Promise<void> | void;
  afterInit?(): Promise<void> | void;
  dispose?(): Promise<void> | void;
}

| Метод | Назначение | |---------------|------------| | required() | Указание зависимостей. Не обязательная, можно в init, но в сложных проектах могут быть циклические ссылки | | init() | Инициализация плагина. Подключение других плагинов, инициализация ресурсов | | afterInit() | Методы, вызываемые после инициализации всех плагинов | | dispose() | Очистка ресурсов, отписка от событий |

Пример плагина

import type { IPlugin, Instance } from '@emailmaker/emailmaker';

class TestPlugin implements IPlugin {
  constructor(readonly _editor: Instance) {}

  async init() {
    try {
      const sandboxScriptApi = this._editor.use('SandboxScriptApi');
      // ...
    } catch (e) {
      console.error("Ошибка инициализации:", e);
    }
  }

  dispose() {
    // Очистка ресурсов
  }
}

Sandbox-часть

Скрипт в iframe (@emailmaker/extensions-sandbox) использует упрощённый lifecycle — без required():

import { App } from '@emailmaker/extensions-sandbox';
import type { IPlugin } from '@emailmaker/extensions-sandbox';

class MyPluginSandbox implements IPlugin {
  init() { /* ... */ }
  afterInit?() { /* ... */ }
  dispose() { /* ... */ }
}

App.registerPlugin('MyPluginSandbox', MyPluginSandbox);

🔌 Подключение плагина

1. Прямой класс (рекомендуемый)

Самый простой способ — передать класс плагина напрямую:

import { MyPlugin } from 'my-plugin-package';

emailmaker.init({
  plugins: [MyPlugin]
});

// С опциями (second argument)
emailmaker.init({
  plugins: [
    [MyPlugin, { publicPath: 'https://cdn.example.com/my-plugin/' }]
  ]
});

Конструктор плагина (класс или фабрика) принимает (context, options?):

class MyPlugin implements IPlugin {
  constructor(private ctx: Instance, private options?: MyOptions) {}
}

// Или фабрика
function myPlugin(ctx: Instance, options?: MyOptions): IPlugin {
  return { init() { /* ... */ } };
}

2. Ленивая загрузка

Плагин загружается асинхронно — удобно для code-splitting:

emailmaker.init({
  plugins: [
    () => import('my-plugin-package').then(m => m.MyPlugin)
  ]
});

Результат — обычный Promise, пользователь сам выбирает нужный экспорт.

3. Декларативная загрузка по URL

Загрузка плагина по URL без явного импорта:

emailmaker.init({
  plugins: [
    { type: 'esm', url: 'https://cdn.example.com/my-plugin.js' },
    { type: 'umd', url: 'https://cdn.example.com/my-plugin.umd.js', name: 'MyPlugin', resolve: 'registry' },
  ]
});

| Параметр | Описание | |----------|----------| | type | 'esm' — ESM модуль (import()), 'umd' — UMD/IIFE (<script>) | | url | URL до скрипта плагина | | name | Для resolve: 'registry' — строковый ключ из PluginTypeMap; для legacy umd/window — ключ в window | | resolve | Опционально. 'module' (default ESM), 'window' (legacy default UMD), 'registry' |

4. Регистрация по имени (связь между плагинами)

Если плагин A должен обращаться к плагину B через editor.use('PluginB') — используйте pluginRegistry.add:

// В пакете плагина
import { pluginRegistry } from '@emailmaker/extensions-app';
import { MyPlugin } from './MyPlugin';

pluginRegistry.add('MyPlugin', MyPlugin);
// В приложении
emailmaker.init({
  plugins: ['MyPlugin']
});
// В другом плагине
const myPlugin = this.editor.use('MyPlugin');

⚠️ Для URL-режимов globals / async-globals используйте resolve: 'registry' и pluginRegistry.add(...). Это делает name проверяемым через PluginTypeMap и убирает зависимость от window[name].

Метод use()

Синхронный метод для получения экземпляра уже загруженного плагина:

// По классу (рекомендуемый — полная типизация)
const myPlugin = this.editor.use(MyPlugin);

// По имени (требует PluginTypeMap и pluginRegistry.add)
const myPlugin = this.editor.use('MyPlugin');

Метод useAsync()

Асинхронный метод для работы с ленивыми загрузчиками и дескрипторами:

const myPlugin = await this.editor.useAsync(
  () => import('my-plugin').then(m => m.MyPlugin)
);

const myPlugin = await this.editor.useAsync(
  { type: 'esm', url: '/plugins/analytics.js' }
);

const myPluginByKey = await this.editor.useAsync(
  { type: 'umd', url: '/plugins/my-plugin.umd.js', name: 'MyPlugin', resolve: 'registry' }
);

📤 API: работа с редактором

ElementsApi

Добавление элемента в боковую панель. Также имеется возможность изменить текущие компоненты или удалить их.

const elementsApi = editor.use('ElementsApi');
elementsApi.insert({
  title: "Product",
  html: "<div>...</div>",
  name: "product-block",
  icon: { type: 'IconSun', props: {} }
});

SettingsPanelApi

Добавление новой панели настроек. Например, можно добавить панель которая будет отображаться при клике на элементе в sandbox.

import { createComponentIdentifier } from '@emailmaker/extensions-app';

const SettingsPanelId = createComponentIdentifier('MySettingsPanel');
// ComponentRegistry.add(SettingsPanelId, MySettingsPanel);

const panel = editor.use('SettingsPanelApi');
panel.showSettingsPanel({
  content: {
    type: SettingsPanelId,
    props: {...}
  },
  caption: 'Настройки',
  deletable: true
});

instanceKey (идентификатор экземпляра панели)

По умолчанию каждый вызов showSettingsPanel получает новый instanceKey, и хост перемонтирует React-компонент панели. Это нужно при переключении между несколькими экземплярами одного блока в письме (две карточки товара с одним ComponentId).

Передайте стабильный instanceKey только если нужно обновлять props без remount и сохранять внутреннее состояние панели (скролл, фокус, черновик формы):

panel.showSettingsPanel({
  instanceKey: blockUuid,
  content: { type: SettingsPanelId, props: { blockUuid, settings } },
});

Не вызывайте closeSettingsPanel перед showSettingsPanel при переключении между экземплярами — достаточно одного showSettingsPanel с новыми props.

Примерная последовательность процесса взаимодействия sandbox и app:

  1. Обработчик клика в sandbox
  2. Отправка события в app через MessageService
  3. Отображение зарегистрированной панели
  4. Отправки изменений из панели в sandbox через MessageService

ModalApi

Интерфейс основан на модалках Ant Design

import { createComponentIdentifier } from '@emailmaker/extensions-app';
import { ComponentRegistry } from '@emailmaker/extensions-react';

const MyPanelId = createComponentIdentifier('MyPanel');
ComponentRegistry.add(MyPanelId, MyPanel);

const modal = editor.use('ModalApi');
modal.show({
  title: 'Выбор группы',
  content: {
    type: MyPanelId,
    props: { groups, settings },
  },
});

Для простого UI можно передать JSX напрямую (см. раздел extensions-react).

EmailSettingsApi

API для работы с настройками письма (редактор email / emailmaker). В widget-сценариях может быть недоступен.

Позволяет получать и изменять настройки внешнего вида письма и стили элементов контента.

API разделен на два типа методов:

  • Layout Settings — настройки письма целиком (фон, ширина, адаптивность)
  • Content Styles — стили элементов контента (текст, заголовки, ссылки, кнопки, блоки, карточки)

Получение и установка настроек внешнего вида письма:

const emailSettingsApi = editor.use('EmailSettingsApi');

// Получить настройки внешнего вида письма
const layoutSettings = await emailSettingsApi.getEmailLayoutSettings();
console.log(layoutSettings.backgroundColor); // цвет фона письма
console.log(layoutSettings.width); // ширина письма {value: 600, dim: 'px'}
console.log(layoutSettings.responsive); // настройки адаптивности

// Изменить настройки внешнего вида письма
await emailSettingsApi.setEmailLayoutSettings({
  backgroundColor: '#ffffff',
  width: { value: 600, dim: 'px' },
  responsive: { shutdown: false, styles: true }
});

Получение и установка стилей элементов контента:

// Получить стили элементов контента
const contentStyles = await emailSettingsApi.getEmailContentStyles();
console.log(contentStyles.text); // стили текста
console.log(contentStyles.buttons); // стили кнопок
console.log(contentStyles.block); // стили блоков

// Изменить стили элементов контента
await emailSettingsApi.setEmailContentStyles({
  text: { fontSize: 18, color: '#333333' },
  buttons: { 
    backgroundColor: '#1890ff', 
    borderRadius: { all: 8 } 
  },
  block: { 
    padding: { top: 20, right: 20, bottom: 20, left: 20 } 
  }
});

⚠️ Методы для работы с layout settings (getEmailLayoutSettings, setEmailLayoutSettings) ждут инициализации письма из iframe перед возвратом или установкой данных, чтобы гарантировать актуальность информации. Методы для работы с content styles не требуют ожидания, так как стили хранятся в объекте письма.

MessageService

Типизированная надстройка над postMessage, для обмена сообщениями между UI и sandbox.

const msg = editor.use('MessageService');
msg.send('MY_EVENT', { value: 123 });

msg.addListener('MY_EVENT', (data) => console.log(data));

Выносите имена событий в общий модуль (CLI-шаблон: src/interfaces/), чтобы app и sandbox использовали одни и те же строки:

// src/interfaces/messages.ts
export const MY_PLUGIN_UPDATE = 'my-plugin:update' as const;

export type MyPluginUpdatePayload = { text: string };
// app
msg.send(MY_PLUGIN_UPDATE, { text: 'hello' });

// sandbox
messageService.addListener(MY_PLUGIN_UPDATE, (data) => { /* ... */ });

Тип payload задайте рядом с константой (MyPluginUpdatePayload) — отдельный runtime-пакет для идентификаторов не нужен.

SandboxScriptApi

Позволяет динамически подключать JS и CSS в iframe, например, для загрузки сторонних библиотек. Также используется для загрузки скрипта, взаимодействующего с DOM письма:

const sandboxApi = editor.use('SandboxScriptApi');

const style = sandboxApi.registerSandboxCSS('https://cdn.com/style.css');
const script = sandboxApi.registerSandboxScript('https://cdn.com/script.js');

// или по коду, если это допустимо CSP
sandboxApi.registerSandboxScriptCode('console.log("hi")');

⚠️ Использование registerSandboxScriptCode может нарушить политику безопасности (CSP). Лучше предпочитать загрузку внешних скриптов через registerSandboxScript().


🧪 Плагины Sandbox

Работают внутри iframe для взаимодействия с DOM письма.

import { App } from '@emailmaker/extensions-sandbox';
import type { IPlugin } from '@emailmaker/extensions-sandbox';

class MyPluginSandbox implements IPlugin {
  init() {
    const messageService = App.use('MessageService');
    
    messageService.addListener('my-plugin:update', (data) => {
      const element = document.querySelector('.my-block');
      if (element) {
        element.textContent = data.text;
        App.use('SyncService').commit();
      }
    });
  }

  dispose() {}
}

App.registerPlugin('MyPluginSandbox', MyPluginSandbox);

⚠️ Sandbox работает в изолированном iframe. React-компоненты здесь недоступны — только нативный DOM.

MessageService (внутри sandbox)

Аналогичен UI-версии. Позволяет слушать и отправлять сообщения.

import { App } from '@emailmaker/extensions-sandbox';

const messageService = App.use('MessageService');

// Отправка в app
messageService.send('my-plugin:click', { elementId: '123' });

// Получение из app
messageService.addListener('my-plugin:update', (data) => {
  console.log('Received:', data);
});

SyncService

Для сохранения изменений из DOM в код письма:

import { App } from '@emailmaker/extensions-sandbox';

const syncService = App.use('SyncService');

document.querySelector('.title').textContent = 'Updated';
syncService.commit();

⚠️ Все несохранённые (незакоммиченные) изменения могут быть утеряны при следующем рендере.


🧱 Регистрация UI компонентов

ComponentRegistry нужен, когда вы хотите зарегистрировать React-компонент и потом передавать его в API по типизированному идентификатору:

import { createComponentIdentifier } from '@emailmaker/extensions-app';
import { ComponentRegistry } from '@emailmaker/extensions-react';

const MyPanelId = createComponentIdentifier<{ groups: Group[] }>('MyPanel');

ComponentRegistry.add(MyPanelId, MyReactPanel);

ComponentRegistry.override(MyPanelId, (Base) => (props) => (
  <div className="bordered"><Base {...props} /></div>
));

Когда использовать

  • если хотите просто показать React UI, чаще всего удобнее передать JSX напрямую
  • если нужен стабильный ComponentId для { type, props } — createComponentIdentifier + ComponentRegistry
  • если UI без React, используйте DomComponentRegistry

Пример с JSX:

import { SettingsPanelApi } from '@emailmaker/extensions-app';
import '@emailmaker/extensions-react';

settingsPanel.showSettingsPanel({
  content: <MyPanel />,
});

Пример с идентификатором:

settingsPanel.showSettingsPanel({
  content: { type: MyPanelId, props: { groups } },
});

🎨 UI Kit

В коде плагина импортируйте компоненты из @emailmaker/ui-kit. В release-сборке PluginDev externalizes ui-kit на singleton хоста: @emailmaker/emailmaker/ui-kit (отдельно ставить main subpath не нужно).

ColorPicker — минимальный пример

Растягивается на ширину родителя (Form.Item, колонку и т.д.) — дополнительный inline-style для ширины не нужен.

import { EmIcons, ColorPicker } from "@emailmaker/ui-kit";

<ColorPicker
  value={settings.bgColor}
  onChange={(v) => onChange({ ...settings, bgColor: v })}
/>

FormColorField — рекомендуется для settings panel

Микролейбл + Form.Item + color picker — тот же паттерн, что в панелях редактора. Используйте внутри Form с полем name.

import { Form, FormColorField } from "@emailmaker/ui-kit";

<Form layout="vertical">
  <FormColorField name="bgColor" label="Цвет фона" />
</Form>

⚛️ extensions-react

@emailmaker/extensions-react нужен для React-плагинов.

Что в нем есть

  • ComponentRegistry
  • createComponentIdentifier()
  • поддержка React-компонентов в API

Что можно делать

После установки пакета вы можете передавать JSX прямо в API:

import { SettingsPanelApi } from '@emailmaker/extensions-app';
import '@emailmaker/extensions-react';

settingsPanel.showSettingsPanel({
  content: <MyPanel />,
});

Когда использовать JSX

Прямой JSX — лучший путь по умолчанию для React-плагина:

settingsPanel.showSettingsPanel({
  content: <MyPanel value={state} />,
});

Он хорош, когда:

  • не нужен стабильный ComponentId
  • не нужен override()
  • UI используется локально в одном месте

Когда использовать ComponentRegistry

ComponentRegistry нужен, если:

  • компонент должен быть доступен по ComponentId
  • нужна возможность override()
  • компонент используется в нескольких местах через { type, props }
  • нужен стабильный идентификатор для контракта между частями плагина

Пример:

import { createComponentIdentifier } from '@emailmaker/extensions-app';
import { ComponentRegistry } from '@emailmaker/extensions-react';

const PanelId = createComponentIdentifier<{ value: string }>('MyPanel');

ComponentRegistry.add(PanelId, MyPanel);

settingsPanel.showSettingsPanel({
  content: {
    type: PanelId,
    props: { value: 'hello' },
  },
});

Короткое правило

  • для React-плагина ставьте @emailmaker/extensions-react
  • для обычного React UI чаще всего достаточно JSX
  • если нужен идентификатор компонента, используйте ComponentRegistry

📦 Расширение типов

PluginTypeMap

PluginTypeMap — опциональный механизм для связи между плагинами по строковому имени. Позволяет использовать editor.use('PluginName') с автодополнением.

Когда нужен:

  • Плагин A обращается к плагину B через editor.use('PluginB')
  • URL-режимы globals / async-globals, если плагин подключается через descriptor с resolve: 'registry'

Когда не нужен:

  • Плагин подключается напрямую через класс: plugins: [MyPlugin]
  • Типизация через editor.use(MyPlugin) (прямая ссылка на класс)
declare module "@emailmaker/emailmaker" {
  interface PluginTypeMap {
    TestPlugin: typeof TestPlugin;
  }
}

PluginTypeMap наследует все базовые плагины из BasePluginTypeMap, поэтому доступны автодополнения для встроенных API (ElementsApi, MessageService, ModalApi и т.д.).

💡 Рекомендуем editor.use(MyPlugin) с типизацией через класс напрямую. PluginTypeMap нужен для editor.use('Name') между плагинами и для typed descriptor-ов вида { type: 'umd', url, name, resolve: 'registry' }.

Опции плагина

Конструктор плагина (класс или фабрика) принимает (context, options?). Для типизации используйте ExtractPluginOptions:

import type { ExtractPluginOptions } from '@emailmaker/emailmaker';

// Опции извлекаются из конструктора
type MyOpts = ExtractPluginOptions<typeof MyPlugin>;

Опции передаются через кортеж в plugins:

emailmaker.init({
  plugins: [
    [MyPlugin, { theme: 'dark' }]
  ]
});

При множественной инициализации действует принцип «first-write wins» — опции фиксируются при первом создании экземпляра.

CLI-шаблон уже собирает типы отдельно (npm run build:types) и публикует dist/public-types.d.ts рядом с JS bundle, поэтому поддержку TypeScript стоит считать частью любого release target, а не только npm/ESM сценария.

Расширение Config

Публикуйте .d.ts вместе с плагином для расширения конфигурации редактора:

declare module "@emailmaker/emailmaker" {
  interface Config {
    productBlock?: {
      enabled?: boolean;
      groups: Group[];
    };
  }
}

React-компоненты в API

Если в проекте подключен @emailmaker/extensions-react, в API можно передавать React-компоненты и JSX напрямую.

Пример:

import { SettingsPanelApi } from '@emailmaker/extensions-app';
import '@emailmaker/extensions-react';

settingsPanel.showSettingsPanel({
  content: <MyPanel />,
});

Если вы пишете React-плагин, просто установите @emailmaker/extensions-react.


🧰 CLI

@emailmaker/cli — публичный генератор проектов: внешние плагины и демо-стенды. Работает в интерактивном режиме и с полным набором аргументов командной строки.

Флаги командной строки

Ниже — опции, которые отражены в справке CLI (--help). Скрытые или служебные поля (монорепозиторий, отладка шаблонов) здесь не перечисляются.

--dev / --dev-packages

Самый частый флаг для разработки: подставляет dev-версии внутренних npm-пакетов (@emailmaker-internal/*), которые в шаблонах задаются с dist-tag dev вместо latest. Используйте, когда нужны свежие предрелизные сборки экосистемы.

npx @emailmaker/cli my-plugin --kind plugin --preset advanced --bundler vite --target npm-package --dev --output-dir ./my-plugin

Короткая форма: --dev (эквивалент --dev-packages).

Остальные опции

| Флаг | Назначение | |------|------------| | --interactive | Запустить интерактивный мастер (шаги вместо обязательных аргументов). | | --package-name | Явное имя npm-пакета для генерируемого плагина. | | --kind | Режим: plugin или demo-stand. | | --stand-mode | Сценарий стенда: oauth, closed-loop, react-adapter, advanced (и связанные варианты). | | --advanced-profile | Для --stand-mode advanced: subpath, shadow-dom, umd. | | --preset | Шаблон плагина: minimal или advanced. | | --bundler | vite или webpack — сборщик в шаблоне. | | --target | Поставка плагина: npm-package, browser, advanced (advanced — только webpack; у Vite доступны npm-package и browser). | | --advanced-target | Для --target advanced (только webpack): async-globals или legacy-async-globals. | | --app | Идентификатор приложения (REACT_APP_NAME), как в монорепозитории; в опубликованном CLI может быть скрыт. | | --output-dir | Каталог, куда положить сгенерированный проект. | | --output-base | Базовый каталог: внутри него будет создана папка проекта. | | --force | Разрешить запись в уже существующую директорию. |

Полный список и актуальные значения перечислены в выводе:

npx @emailmaker/cli --help

Что умеет CLI

  • интерактивный wizard и неинтерактивный запуск;
  • генерация проекта плагина и демо-стендов;
  • шаблоны под vite и webpack;
  • выбор способа поставки плагина: npm-модуль, браузерный bundle (browser), режим advanced (webpack-only).

Основные режимы

plugin

Внешний плагин: app-часть, sandbox, локальный debug host, базовые скрипты сборки.

demo-stand (в т.ч. stand, stand-closed в документации)

Отдельный стенд для интеграции: варианты с OAuth, закрытым контуром, react-adapter, advanced-профили.

Preset-ы для plugin

  • minimal — каркас без React-примера.
  • advanced — React UI, ComponentRegistry, примеры панелей и API.

Примеры

С dev-пакетами:

npx @emailmaker/cli my-plugin --kind plugin --preset advanced --bundler vite --target npm-package --dev --output-dir ./my-plugin

Без dev (стабильные версии из реестра):

npx @emailmaker/cli my-plugin --kind plugin --preset advanced --bundler vite --target npm-package --output-dir ./my-plugin

После глобальной установки команда может называться emailmaker-cli.

Когда использовать CLI

  • быстро создать новый внешний плагин;
  • не собирать вручную конфиг vite / webpack;
  • получить согласованную структуру app, sandbox и локальной отладки.

🧯 Troubleshooting

dist неожиданно толстый

Проверьте сначала:

  • что release target выбран как npm-пакет или встраиваемый bundle, а не advanced
  • что release config использует PluginDev(...), а не ручной externals
  • что в bundle не попали react, antd или runtime subpath-ы как обычные модули

Частая причина: release config собирается без корректного externals/alias режима.

Плагин работает в start, но ломается в release

Обычно это значит, что dev host использует PluginDev({ externals: false }), а release target уже требует runtime contract.

Проверьте локально через CLI-шаблон:

npm run build
npm run start:release

Также проверьте:

  • target сборки в CLI / config
  • release config для src/index.ts
  • способ загрузки плагина в host (npm-пакет vs встраиваемый bundle)

Browser target не загружается

Проверьте:

  • что host заранее загрузил runtime globals
  • что release bundle собран под target встраиваемый bundle
  • что sandbox и main bundle публикуются по ожидаемым URL

Duplicate React / hooks error

Почти всегда это значит, что React попал в bundle плагина вместо runtime externals.

Проверьте:

  • используется ли PluginDev в release config
  • не отключён ли externals вручную
  • нет ли дополнительных alias/resolve правил, которые обходят runtime contract

Когда использовать advanced

Только если host уже требует Webpack async/globals compatibility.

Для обычного внешнего плагина:

  • npm-пакет — основной путь
  • встраиваемый bundle — для script/UMD загрузки
  • advanced — только для совместимости, когда вы точно знаете контракт host

❓ FAQ / Типичные ошибки

  • «Invalid hook call» или два React на странице

    Для обычного плагина используйте стандартные импорты react / antd и собирайте проект через PluginDev с alias: true (по умолчанию). Не смешивайте обычные импорты с прямыми импортами из runtime в одном plugin UI.

  • Externals не работают в dev server

    В serve-режиме externals должны быть отключены: PluginDev({ externals: false }). Зависимости резолвятся из runtime через alias.

  • ESM output не грузится через обычный <script>

    ESM-сборка содержит import. Используйте <script type="module">, npm-интеграцию или target browser (PluginDev({ externals: 'globals' }), UMD bundle).

  • Browser/UMD-плагин: «Cannot read property of undefined»

    Runtime хоста должен быть загружен до плагина. Подключайте через npm + lazy import, либо декларативно { type: 'umd', url: '…', name: 'MyPlugin', resolve: 'registry' } (с pluginRegistry.add).

  • Не загружаются JS/CSS / iframe не отображается

    Проверьте publicPath и подключение основного плагина (VitePlugin / WebpackPlugin). В serve-режиме он обслуживает статику из node_modules/.

  • Панель или компонент не отображается

    Убедитесь в регистрации через ComponentRegistry и createComponentIdentifier.

  • Событие не обрабатывается

    Проверьте подписку на событие в MessageService и совпадение строковых id в app и sandbox.

  • Изменения в iframe теряются

    Не забывайте вызывать syncService.commit().


🔄 Миграция с предыдущих версий

С externals.json на PluginDev:

Для новых проектов используйте только PluginDev. Файл externals.json в экосистеме остаётся legacy-следом; не подключайте его вручную в Vite-шаблонах.

С pluginRegistry.add на прямой класс:

До:

pluginRegistry.add('MyPlugin', MyPlugin);
// init: plugins: ['MyPlugin']

После (если не нужен editor.use('MyPlugin') между плагинами):

// init: plugins: [MyPlugin]

🔗 Полезные ссылки