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/extensions-app

v1.1.102

Published

Thin public facade for the Emailmaker extensions (plugin) API.

Readme

📦 Система плагинов для 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(), preload(), типы 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

globals читает зависимости из глобального реестра синхронно, когда выполняется скрипт плагина. Хост готовит реестр сам, если грузит плагин по URL через init() или useAsync(); при ручной вставке <script> дождитесь preload() — подробности в разделе Подключение плагина.

Для встраиваемый 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

Иконки — особый случай. Общим (singleton) обязан быть только класс @ant-design/icons/es/components/AntdIcon: он вычисляет иконку, читая IconContext, и вторая его копия сломает тему. Сам каталог из восьми с лишним сотен определений singleton'ом не является — каждый потребитель берёт его себе и вытрясает неиспользуемое. Поэтому обычный import { UserOutlined } from '@ant-design/icons' в плагине законен: в сборку попадут только взятые иконки, а общая машинерия придёт из продукта. PluginDev разводит эти два случая сам.

Если 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 вручную.


Совместимость: как плагин встречается с редактором

Одна страница на вопрос «что с чем работает». Всё остальное в этом разделе — подробности.

Главное правило

Плагин и редактор обязаны использовать один экземпляр React, antd и ui-kit. Не потому, что так аккуратнее: компоненты плагина рисуются внутри дерева редактора, и если экземпляры разные, хуки вызываются против чужого диспетчера. На странице это выглядит как Cannot read properties of null (reading 'useState') — падает не панель плагина, а весь интерфейс.

Сводить их вручную не нужно: этим занимается PluginDev в сборке плагина.

Формат плагина × способ подключения

| Формат плагина | Как подключается | Где берёт React, antd, ui-kit | |---|---|---| | ESM (npm-пакет, собирается вместе с хостом) | импортом: plugins: [MyPlugin] или useAsync(() => import(…)) | PluginDev переписывает импорты на подпути продукта | | UMD / globals (файл по URL) | { type: 'umd', url, name, resolve: 'registry' } | из реестра window.emailmaker.modules на странице | | ~~ESM по URL~~ | не поддерживается | — |

Почему { type: 'esm', url } не работает. В собранном ESM-плагине остаются имена подпутей продукта (@emailmaker/emailmaker/runtime/*). Браузеру разрешать их нечем: карту импортов продукт не выдаёт. Плагин в формате ESM подключают как npm-пакет, собирая вместе с хостом.

Что приходит из продукта, а что собирается в плагин

| Зависимость | Откуда берётся | Почему так | |---|---|---| | react, react-dom | из продукта (@emailmaker/emailmaker/runtime/react) | один диспетчер хуков на страницу | | antd | из продукта | контексты темы и формы общие | | @emailmaker/ui-kit | из продукта (@emailmaker/emailmaker/ui-kit) | общий ThemeProvider, стили вставляются один раз | | @ant-design/icons — класс AntdIcon | из продукта | читает IconContext; вторая копия ломает тему | | @ant-design/icons — каталог иконок | собирается в плагин | каждый берёт только те иконки, что использует | | всё остальное | собирается в плагин | к общему состоянию отношения не имеет |

Как проверить, что всё сошлось

Сборка хоста сама скажет, если в граф попала вторая копия React или ui-kit: плагин хоста (Vite/webpack) печатает предупреждение с именем файла-виновника и обоими путями. Молчание на сборке — признак того, что копии сведены.

Плагин на глобалах при этом ничего не импортирует и в графе не появляется: он берёт зависимости из реестра уже на странице, и проверять там нечего.

Вход редактора тоже участвует

Редактор подключают двумя способами, и не с каждым работает ESM-плагин:

| Вход редактора | ESM-плагин | Плагин на глобалах | |---|---|---| | ESM (собирается вместе с хостом) | да | да | | UMD (<script> на странице) | нет | да |

Почему UMD-вход и ESM-плагин не сходятся. UMD-вход несёт React внутри себя и объявляет его на странице сам. ESM-плагин собирает ваш сборщик, и подпуть @emailmaker/emailmaker/runtime/react он разрешает в копию внутри пакета. Это два разных файла с двумя диспетчерами хуков — тот самый Cannot read properties of null (reading 'useState'), падающий вместе со всем интерфейсом.

Сборка про это молчать обязана: UMD-редактор подключён скриптом и в граф сборки хоста не входит, а в момент сборки плагина ещё неизвестно, какой вход выберет хост. Поэтому редактор говорит сам — при инициализации он сверяет копии и, найдя вторую, пишет в консоль, что нашёл и как это чинить.

Чинится любым из двух способов: собрать плагин на глобалах — тогда он возьмёт React из реестра window.emailmaker.modules на странице, — либо подключить ESM-вход редактора и собрать оба одним сборщиком.

Что бывает, если собрать плагин без PluginDev

Плагин соберётся, сборка хоста пройдёт — и редактор упадёт на странице. Ровно этот случай:

  • в плагине остались import 'react' и import '@emailmaker/ui-kit' как есть;
  • сборщик хоста разрешил их из node_modules проекта — это вторые копии;
  • редактор при этом работает на своих, вендоренных.

Лечится добавлением PluginDev в конфигурацию сборки плагина (externals: 'esm') либо сборкой плагина на глобалах.

Ваши настройки резолва редактора не касаются

Внутри пакета React адресуется путём к копии, которая лежит в нём же, а не именем react. Следствий два, и оба в вашу пользу:

  • resolve.dedupe, resolve.alias, resolve.modules и Module Federation shared работают у вас так, как вы их написали. Мы не занимаем имя react и ничего у вас не перехватываем.
  • Редактор не зависит от того, что вы с этим именем делаете: своя копия достаётся ему при любой конфигурации сборки, в том числе если наш плагин у вас не подключён.

Одно исключение — sharedReact ниже: им вы просите обратного.

Общий React (необязательно)

Плагин хоста умеет обратное: отдать редактору ваш React вместо вендоренного — VitePlugin({ sharedReact: true }) или new WebpackPlugin({ sharedReact: true }).

| | по умолчанию | sharedReact: true | |---|---|---| | React редактора | свой, внутри пакета | ваш | | требования к вашему React | нет | мажорная версия 19 | | Strict Mode и dev-сборка React | не касаются редактора | касаются | | antd | свой, внутри пакета | свой, внутри пакета | | вес поставки | как есть | меньше примерно на 98 КБ gzip |

Версия проверяется до сборки: несовпадение мажора будет названо в предупреждении.


📂 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: 'umd', url: 'https://cdn.example.com/my-plugin.umd.js', name: 'MyPlugin', resolve: 'registry' },
  ]
});

По URL работает umd с resolve: 'registry'. Вариант { type: 'esm', url } в браузере не поддерживается: в собранном ESM-плагине остаются имена подпутей продукта (@emailmaker/emailmaker/runtime/*), а разрешить их браузеру нечем — карту импортов продукт не выдаёт. ESM-плагин подключают как npm-пакет: способами 1 и 2 выше, собирая его вместе с хостом.

| Параметр | Описание | |----------|----------| | 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' |

Кто готовит runtime до запуска скрипта

Плагин, собранный под globals (targets встраиваемый bundle и advanced), читает window.__emailmaker__.modules.Antd синхронно — в момент, когда выполняется его скрипт. До этого момента в реестре лежат ожидания: часть runtime приезжает по требованию, чтобы первый экран не платил за то, чем не пользуется.

Если плагин подключён через init({ plugins: [...] }) или useAsync(...), делать ничего не нужно: хост сам дожидается готовности runtime перед тем, как выполнить скрипт. Тормозится только загрузка такого плагина, остальное приложение работает своим ходом.

Если же вы вставляете <script> с плагином сами, мимо API — дождитесь preload():

import { preload } from '@emailmaker/emailmaker';

await preload();
document.head.appendChild(Object.assign(document.createElement('script'), {
  src: 'https://cdn.example.com/my-plugin.umd.js',
}));

Плагинов на esm это не касается: их импорты разрешает сборщик, ждать им нечего.

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 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.

Скрипт живёт одно письмо

Документ холста пересоздаётся на каждое письмо: скрипт песочницы выполняется заново, и ничего из прошлого письма в нём не остаётся. handle.ready — обещание, оно разрешается один раз, для первого холста. Чтобы узнавать о готовности каждый раз, подпишитесь на onReady:

const script = sandboxApi.registerSandboxScript(url);
script.onReady(() => {
  messageService.send('my-plugin:settings', mySettings);
});

Последнее, что вы отправили скрипту после его готовности, редактор повторяет в новый холст сам — по каждому ключу сообщения. onReady нужен для состояния, которое вы считаете заново.

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>

Значки

Значки ui-kit берите по одному из подпути icons — в сборку плагина попадут только те, что вы используете. В release-сборке этот путь уходит на @emailmaker/emailmaker/ui-kit/icons хоста.

import { AIRequests, Sun } from "@emailmaker/ui-kit/icons";

<AIRequests />

<EmIcons icon="Sun" /> тоже работает, но подтягивает весь каталог значков и пишет в консоль предупреждение.


⚛️ 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

Самый частый флаг для разработки: в сгенерированном проекте пакеты экосистемы получают 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

Плагин на globals падает: вместо antd приходит объект с then

Значит скрипт плагина выполнился раньше, чем хост подготовил реестр runtime. Часть runtime приезжает по требованию, и до этого момента в window.__emailmaker__.modules лежат ожидания, а не значения.

Проверьте:

  • плагин подключён через init({ plugins: [{ type, url, ... }] }) или useAsync(...) — тогда хост дожидается готовности сам;
  • если <script> вставляете вы, перед вставкой вызван await preload().

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 в одном интерфейсе плагина: выбирается что-то одно на весь плагин, подробности — в README пакета runtime.

  • 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]

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