@emailmaker/extensions-app 0.9.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1082 @@
1
+ # 📦 Система плагинов для [emailmaker](https://www.npmjs.com/package/@emailmaker/emailmaker)
2
+
3
+ Библиотека предоставляет расширяемую архитектуру для создания плагинов для визуального редактора писем emailmaker. Плагины позволяют:
4
+
5
+ - расширять функциональность редактора;
6
+ - разрабатывать собственные UI-компоненты с использованием экспортированных версий React и Ant Design
7
+ - взаимодействовать с кодом письма в iframe;
8
+ - отображать модальные окна на основе Ant Design в редакторе;
9
+
10
+ Разработка осуществляется с полной поддержкой **TypeScript**, что обеспечивает строгость типов, модульность и предсказуемость.
11
+
12
+
13
+ ---
14
+
15
+ ## ⚙️ Требования
16
+
17
+ - Современный браузер с поддержкой **ES2015** (и выше)
18
+ - Node.js >= 14
19
+ - React и Ant Design подключаются через [`externals`](https://webpack.js.org/configuration/externals/) или напрямую из `@emailmaker/runtime`
20
+
21
+
22
+ ---
23
+
24
+ ## 📘 Термины
25
+
26
+ - **sandbox** — часть плагина, работающая внутри iframe, для взаимодействия с содержимым письма.
27
+ - **app** — часть плагина, встроенная в редактор. Отвечает за интерфейс.
28
+ - **pluginRegistry** — механизм регистрации плагинов.
29
+ - **externals** — [способ подключения внешних библиотек в webpack](https://webpack.js.org/configuration/externals/).
30
+
31
+
32
+ ---
33
+
34
+ ## 🔰 Быстрый старт
35
+
36
+ 👉 Самый простой способ начать — использовать [готовый шаблон](https://github.com/emailmaker/simple_plugin):
37
+
38
+ ```bash
39
+ git clone https://github.com/emailmaker/simple_plugin
40
+ cd simple_plugin
41
+ npm install
42
+ npm run start
43
+ ```
44
+
45
+ Шаблон включает в себя:
46
+ - Webpack / Vite
47
+ - React + Ant Design
48
+ - TypeScript
49
+ - Структуру проекта, описанную ниже
50
+
51
+
52
+ ---
53
+
54
+ ## 🛠 Установка
55
+
56
+ ### 📦 Основные пакеты
57
+
58
+ ```bash
59
+ npm install @emailmaker/emailmaker @emailmaker/runtime @emailmaker/extensions-app @emailmaker/extensions-react @emailmaker/extensions-sandbox
60
+ ```
61
+
62
+ | Пакет | Назначение | Что экспортирует |
63
+ |-------|------------|------------------|
64
+ | `@emailmaker/emailmaker` | Основной пакет | `init()`, `prefetch()`, типы `IPlugin`, `Instance` |
65
+ | `@emailmaker/runtime` | Общие зависимости | React, ReactDOM, Ant Design и связанные модули |
66
+ | `@emailmaker/extensions-app` | API для app-плагинов | `pluginRegistry`, `DomComponentRegistry`, `ElementsApi`, `SettingsPanelApi`, `ModalApi` |
67
+ | `@emailmaker/extensions-react` | React API для плагинов | `ComponentRegistry`, `createComponentIdentifier()` |
68
+ | `@emailmaker/extensions-sandbox` | API для sandbox-плагинов | `App`, `MessageService`, `SyncService` |
69
+
70
+ `@emailmaker/ui-kit` ставится отдельно, если плагин использует готовые UI-компоненты платформы.
71
+
72
+ ### Импорты
73
+
74
+ ```typescript
75
+ // React и Antd — обычные импорты
76
+ import React from 'react';
77
+ import { Button } from 'antd';
78
+
79
+ // UI-компоненты платформы (опционально)
80
+ import { ColorPicker } from '@emailmaker/ui-kit';
81
+
82
+ // Core API плагинов
83
+ import { pluginRegistry, ElementsApi } from '@emailmaker/extensions-app';
84
+
85
+ // React-пакет для плагинов
86
+ import { createComponentIdentifier } from '@emailmaker/extensions-app';
87
+ import { ComponentRegistry } from '@emailmaker/extensions-react';
88
+
89
+ // Типы из основного пакета
90
+ import type { IPlugin, Instance } from '@emailmaker/emailmaker';
91
+ ```
92
+
93
+ ## Что обычно ставить
94
+
95
+ - если у вас обычный React-плагин, ставьте все пакеты из команды выше
96
+ - если используете UI Kit, добавьте еще `@emailmaker/ui-kit`
97
+ - если пишете только sandbox-часть, нужен `@emailmaker/extensions-sandbox`
98
+
99
+ > Для React-плагинов пакет `@emailmaker/extensions-react` нужен по умолчанию.
100
+
101
+
102
+ ---
103
+
104
+ ## 🔧 Сборка плагина
105
+
106
+ Используйте `PluginDev` для настройки бандлера. Он делает две вещи:
107
+ 1. **alias** — настраивает React и Ant Design для работы в плагине
108
+ 2. **externals** — выносит общие зависимости из бандла
109
+
110
+ ```bash
111
+ npm install @emailmaker/emailmaker
112
+ ```
113
+
114
+ ### Таргеты для автора плагина
115
+
116
+ | Таргет | Что получится | Внутри использует |
117
+ |--------|----------------|-------------------|
118
+ | **npm-пакет** | ESM bundle для package-based интеграции | `'esm'` |
119
+ | **Встраиваемый bundle** | UMD/script-подключение в host | `'globals'` |
120
+ | **Advanced** | Webpack-only async globals compatibility | `'async-globals'` / `'legacy-async-globals'` |
121
+
122
+ `PluginDev({ externals: false })` используется только для local dev host и не считается release target.
123
+
124
+ ### Типичный проект: serve + build
125
+
126
+ Самый частый сценарий — плагин в отдельном проекте с двумя режимами:
127
+
128
+ | Режим | Entry | Externals | Основной плагин |
129
+ |-------|-------|-----------|-----------------|
130
+ | serve | `src/dev.ts` | `false` | Да (статика, iframe) |
131
+ | build | `src/index.ts` | зависит от выбранного target | Нет |
132
+
133
+ ```typescript
134
+ // src/dev.ts — тестовая страница с основным приложением
135
+ import '@emailmaker/emailmaker';
136
+ import { MyPlugin } from './index';
137
+
138
+ emailmaker.init({ plugins: [MyPlugin] });
139
+ ```
140
+
141
+ **Vite:**
142
+
143
+ ```javascript
144
+ import VitePlugin from '@emailmaker/emailmaker/vite';
145
+ import PluginDev from '@emailmaker/emailmaker/vite/pluginDev';
146
+
147
+ export default defineConfig(({ command }) => ({
148
+ plugins: [
149
+ command === 'serve' && VitePlugin(),
150
+ PluginDev({ externals: command === 'build' ? 'esm' : false }),
151
+ ],
152
+ build: {
153
+ lib: { entry: 'src/index.ts', formats: ['es'] },
154
+ },
155
+ }));
156
+ ```
157
+
158
+ **Webpack:**
159
+
160
+ ```javascript
161
+ const WebpackPlugin = require('@emailmaker/emailmaker/webpack');
162
+ const PluginDev = require('@emailmaker/emailmaker/webpack/pluginDev');
163
+ const isDev = process.env.NODE_ENV === 'development';
164
+
165
+ module.exports = {
166
+ entry: isDev ? './src/dev.ts' : './src/index.ts',
167
+ plugins: [
168
+ isDev && new WebpackPlugin(),
169
+ new PluginDev({ externals: isDev ? false : 'esm' }),
170
+ ].filter(Boolean),
171
+ };
172
+ ```
173
+
174
+ > `VitePlugin` / `WebpackPlugin` — основной плагин для хост-приложений. В serve обслуживает iframe, шрифты и статику. В build — копирует их в output.
175
+
176
+ ### Опции PluginDev
177
+
178
+ | Опция | По умолчанию | Значения |
179
+ |-------|-------------|----------|
180
+ | `externals` | `'esm'` | `'esm'`, `'globals'`, `'async-globals'` (Webpack), `false` |
181
+ | `alias` | `mode-aware` | `true` / `false` |
182
+
183
+ Для большинства внешних плагинов не выбирайте `externals` вручную — используйте CLI target:
184
+
185
+ - `npm-пакет` -> `esm`
186
+ - `встраиваемый bundle` -> `globals`
187
+ - `advanced` -> только если host уже требует async/globals контракт Webpack
188
+
189
+ Для `встраиваемый bundle` и `advanced` рекомендуемый runtime-контракт — descriptor вида `{ type: 'umd', url, name, resolve: 'registry' }`, где `name` совпадает с ключом из `PluginTypeMap`.
190
+
191
+ ### Ручная настройка (без PluginDev)
192
+
193
+ Нужна только для продвинутой интеграции или собственного бандлерного пайплайна. Основной сценарий для внешнего плагина — отдельный пакет с `PluginDev`.
194
+
195
+ - `resolve.alias`: `react` → `@emailmaker/runtime/react` (и т.д.) для `esm` / dev; локальные shared internals идут через compat facades runtime
196
+ - `externals`: `@emailmaker/runtime/core`, runtime npm-модули и `@emailmaker/ui-kit`
197
+ - Список модулей: `@emailmaker/runtime/plugin-dev-manifest.json` → `runtimeModules`
198
+
199
+ Если release bundle неожиданно стал толстым или target `встраиваемый bundle` не загружается, начните с раздела `Troubleshooting`.
200
+
201
+ Типы для плагина всё равно собираются отдельно: CLI-шаблон запускает `build:types` и кладёт `dist/index.d.ts` рядом с JS bundle, поэтому TypeScript support сохраняется и для browser/advanced targets.
202
+
203
+ ### tsconfig.json
204
+
205
+ ```json
206
+ {
207
+ "compilerOptions": {
208
+ "jsx": "react-jsx"
209
+ }
210
+ }
211
+ ```
212
+
213
+ ## Что писать в коде
214
+
215
+ В коде плагина пишите обычные импорты:
216
+
217
+ ```ts
218
+ import React from 'react';
219
+ import { Button } from 'antd';
220
+ ```
221
+
222
+ А React API для плагинов берите из `@emailmaker/extensions-react`:
223
+
224
+ ```ts
225
+ import { createComponentIdentifier } from '@emailmaker/extensions-app';
226
+ import { ComponentRegistry } from '@emailmaker/extensions-react';
227
+ ```
228
+
229
+ > Обычно достаточно использовать готовый шаблон CLI и не настраивать alias/external вручную.
230
+
231
+
232
+ ---
233
+
234
+ ## 📂 PublicPath
235
+
236
+ `publicPath` используется для указания базового URL для загрузки второстепенных скриптов и статики плагина.
237
+ Более подробно прочитать про publicPath можно в секции [publicPath документации webpack](https://webpack.js.org/guides/public-path/)
238
+
239
+ Внутри плагина:
240
+
241
+ ```ts
242
+ export interface MyPluginOptions {
243
+ publicPath?: string;
244
+ }
245
+
246
+ class MyPlugin implements IPlugin {
247
+ constructor(
248
+ private editor: Instance,
249
+ private options: MyPluginOptions = {}
250
+ ) {}
251
+
252
+ init() {
253
+ if (this.options.publicPath) {
254
+ const sandboxApi = this.editor.use('SandboxScriptApi');
255
+ sandboxApi.registerSandboxScript(this.options.publicPath + 'sandbox.js');
256
+ }
257
+ }
258
+ }
259
+ ```
260
+
261
+ Подключение плагина с publicPath:
262
+
263
+ ```ts
264
+ emailmaker.init({
265
+ plugins: [
266
+ ['MyPlugin', { publicPath: 'https://cdn.example.com/my-plugin/' }]
267
+ ]
268
+ });
269
+ ```
270
+
271
+
272
+ ---
273
+
274
+ ## 📁 Структура проекта
275
+
276
+ ```txt
277
+ plugin-root/
278
+ ├── shared/ # Общие типы и интерфейсы
279
+ │ └── interfaces.ts # Идентификаторы сообщений
280
+ ├── app/ # UI-часть, взаимодействие с редактором
281
+ │ └── MyPlugin.ts # Регистрация и логика плагина
282
+ ├── sandbox/ # Работа с DOM письма
283
+ │ └── index.ts # Точка входа для sandbox
284
+ ├── test/ # Точка входа для тестирования
285
+ ```
286
+
287
+ - `shared` — общие типы для взаимодействия между `app` и `sandbox`
288
+ - `app` — точка входа, из которой можно подключить скрипт sandbox:
289
+
290
+ ```ts
291
+ const sandboxApi = this.editor.use('SandboxScriptApi');
292
+ sandboxApi.registerSandboxScript(this.options.publicPath + 'sandbox.js');
293
+ ```
294
+
295
+ - `sandbox/index.ts` — точка входа в скрипт, взаимодействующий с DOM письма. Скрипт должен быть собран отдельной точкой входа, так как он исполняется в изолированной среде iframe.
296
+ - `test/index.tsx` — пример локального запуска редактора `emailmaker` с загрузкой плагина из `app`
297
+
298
+
299
+ ---
300
+
301
+ ## 🧩 Жизненный цикл плагина
302
+
303
+ Плагин может быть реализован в виде класса или фабрики. В конструкторе класс получает экземпляр редактора `emailmaker` и должен соответствовать интерфейсу:
304
+
305
+ ```ts
306
+ export interface IPlugin {
307
+ required?(): void;
308
+ init?(): Promise<void> | void;
309
+ afterInit?(): Promise<void> | void;
310
+ dispose?(): Promise<void> | void;
311
+ }
312
+ ```
313
+
314
+ | Метод | Назначение |
315
+ |---------------|------------|
316
+ | `required()` | Указание зависимостей. Не обязательная, можно в init, но в сложных проектах могут быть циклические ссылки |
317
+ | `init()` | Инициализация плагина. Подключение других плагинов, инициализация ресурсов |
318
+ | `afterInit()` | Методы, вызываемые после инициализации всех плагинов |
319
+ | `dispose()` | Очистка ресурсов, отписка от событий |
320
+
321
+ ### Пример плагина
322
+
323
+ ```ts
324
+ import type { IPlugin, Instance } from '@emailmaker/emailmaker';
325
+
326
+ class TestPlugin implements IPlugin {
327
+ constructor(readonly _editor: Instance) {}
328
+
329
+ async init() {
330
+ try {
331
+ const sandboxScriptApi = this._editor.use('SandboxScriptApi');
332
+ // ...
333
+ } catch (e) {
334
+ console.error("Ошибка инициализации:", e);
335
+ }
336
+ }
337
+
338
+ dispose() {
339
+ // Очистка ресурсов
340
+ }
341
+ }
342
+ ```
343
+
344
+
345
+ ---
346
+
347
+ ## 🔌 Подключение плагина
348
+
349
+ ### 1. Прямой класс (рекомендуемый)
350
+
351
+ Самый простой способ — передать класс плагина напрямую:
352
+
353
+ ```ts
354
+ import { MyPlugin } from 'my-plugin-package';
355
+
356
+ emailmaker.init({
357
+ plugins: [MyPlugin]
358
+ });
359
+
360
+ // С опциями (second argument)
361
+ emailmaker.init({
362
+ plugins: [
363
+ [MyPlugin, { publicPath: 'https://cdn.example.com/my-plugin/' }]
364
+ ]
365
+ });
366
+ ```
367
+
368
+ Конструктор плагина (класс или фабрика) принимает `(context, options?)`:
369
+
370
+ ```ts
371
+ class MyPlugin implements IPlugin {
372
+ constructor(private ctx: Instance, private options?: MyOptions) {}
373
+ }
374
+
375
+ // Или фабрика
376
+ function myPlugin(ctx: Instance, options?: MyOptions): IPlugin {
377
+ return { init() { /* ... */ } };
378
+ }
379
+ ```
380
+
381
+ ### 2. Ленивая загрузка
382
+
383
+ Плагин загружается асинхронно — удобно для code-splitting:
384
+
385
+ ```ts
386
+ emailmaker.init({
387
+ plugins: [
388
+ () => import('my-plugin-package').then(m => m.MyPlugin)
389
+ ]
390
+ });
391
+ ```
392
+
393
+ > Результат — обычный Promise, пользователь сам выбирает нужный экспорт.
394
+
395
+ ### 3. Декларативная загрузка по URL
396
+
397
+ Загрузка плагина по URL без явного импорта:
398
+
399
+ ```ts
400
+ emailmaker.init({
401
+ plugins: [
402
+ { type: 'esm', url: 'https://cdn.example.com/my-plugin.js' },
403
+ { type: 'umd', url: 'https://cdn.example.com/my-plugin.umd.js', name: 'MyPlugin', resolve: 'registry' },
404
+ ]
405
+ });
406
+ ```
407
+
408
+ | Параметр | Описание |
409
+ |----------|----------|
410
+ | `type` | `'esm'` — ESM модуль (`import()`), `'umd'` — UMD/IIFE (`<script>`) |
411
+ | `url` | URL до скрипта плагина |
412
+ | `name` | Для `resolve: 'registry'` — строковый ключ из `PluginTypeMap`; для legacy `umd`/`window` — ключ в `window` |
413
+ | `resolve` | Опционально. `'module'` (default ESM), `'window'` (legacy default UMD), `'registry'` |
414
+
415
+ ### 4. Регистрация по имени (связь между плагинами)
416
+
417
+ Если плагин A должен обращаться к плагину B через `editor.use('PluginB')` — используйте `pluginRegistry.add`:
418
+
419
+ ```ts
420
+ // В пакете плагина
421
+ import { pluginRegistry } from '@emailmaker/extensions-app';
422
+ import { MyPlugin } from './MyPlugin';
423
+
424
+ pluginRegistry.add('MyPlugin', MyPlugin);
425
+ ```
426
+
427
+ ```ts
428
+ // В приложении
429
+ emailmaker.init({
430
+ plugins: ['MyPlugin']
431
+ });
432
+ ```
433
+
434
+ ```ts
435
+ // В другом плагине
436
+ const myPlugin = this.editor.use('MyPlugin');
437
+ ```
438
+
439
+ > ⚠️ Для URL-режимов `globals` / `async-globals` используйте `resolve: 'registry'` и `pluginRegistry.add(...)`. Это делает `name` проверяемым через `PluginTypeMap` и убирает зависимость от `window[name]`.
440
+
441
+ ### Метод `use()`
442
+
443
+ Синхронный метод для получения экземпляра уже загруженного плагина:
444
+
445
+ ```ts
446
+ // По классу (рекомендуемый — полная типизация)
447
+ const myPlugin = this.editor.use(MyPlugin);
448
+
449
+ // По имени (требует PluginTypeMap и pluginRegistry.add)
450
+ const myPlugin = this.editor.use('MyPlugin');
451
+ ```
452
+
453
+ ### Метод `useAsync()`
454
+
455
+ Асинхронный метод для работы с ленивыми загрузчиками и дескрипторами:
456
+
457
+ ```ts
458
+ const myPlugin = await this.editor.useAsync(
459
+ () => import('my-plugin').then(m => m.MyPlugin)
460
+ );
461
+
462
+ const myPlugin = await this.editor.useAsync(
463
+ { type: 'esm', url: '/plugins/analytics.js' }
464
+ );
465
+
466
+ const myPluginByKey = await this.editor.useAsync(
467
+ { type: 'umd', url: '/plugins/my-plugin.umd.js', name: 'MyPlugin', resolve: 'registry' }
468
+ );
469
+ ```
470
+
471
+
472
+ ---
473
+
474
+ ## 📤 API: работа с редактором
475
+
476
+ ### ElementsApi
477
+
478
+ Добавление элемента в боковую панель. Также имеется возможность изменить текущие компоненты или удалить их.
479
+
480
+ ```ts
481
+ const elementsApi = editor.use('ElementsApi');
482
+ elementsApi.insert({
483
+ title: "Product",
484
+ html: "<div>...</div>",
485
+ name: "product-block",
486
+ icon: { type: 'IconSun', props: {} }
487
+ });
488
+ ```
489
+
490
+
491
+ ### SettingsPanelApi
492
+
493
+ Добавление новой панели настроек. Например, можно добавить панель которая будет отображаться при клике на элементе в sandbox.
494
+
495
+ ```ts
496
+ const panel = editor.use('SettingsPanelApi');
497
+ panel.showSettingsPanel({
498
+ content: {
499
+ type: 'SettingsPanelId',
500
+ props: {...}
501
+ },
502
+ caption: 'Настройки',
503
+ deletable: true
504
+ });
505
+ ```
506
+
507
+ Примерная последовательность процесса взаимодействия sandbox и app:
508
+ 1. Обработчик клика в sandbox
509
+ 2. Отправка события в app через MessageService
510
+ 3. Отображение зарегистрированной панели
511
+ 4. Отправки изменений из панели в sandbox через MessageService
512
+
513
+
514
+ ### ModalApi
515
+
516
+ > Интерфейс основан на модалках `Ant Design`
517
+
518
+ ```ts
519
+ const modal = editor.use('ModalApi');
520
+ modal.show({
521
+ title: 'Выбор группы',
522
+ content: {
523
+ type: 'MyPanel',
524
+ props: { groups, settings }
525
+ }
526
+ });
527
+ ```
528
+
529
+
530
+ ### EmailSettingsApi
531
+
532
+ > API для работы с настройками письма. Позволяет получать и изменять настройки внешнего вида письма и стили элементов контента.
533
+
534
+ API разделен на два типа методов:
535
+ - **Layout Settings** — настройки письма целиком (фон, ширина, адаптивность)
536
+ - **Content Styles** — стили элементов контента (текст, заголовки, ссылки, кнопки, блоки, карточки)
537
+
538
+ #### Получение и установка настроек внешнего вида письма:
539
+
540
+ ```ts
541
+ const emailSettingsApi = editor.use('EmailSettingsApi');
542
+
543
+ // Получить настройки внешнего вида письма
544
+ const layoutSettings = await emailSettingsApi.getEmailLayoutSettings();
545
+ console.log(layoutSettings.backgroundColor); // цвет фона письма
546
+ console.log(layoutSettings.width); // ширина письма {value: 600, dim: 'px'}
547
+ console.log(layoutSettings.responsive); // настройки адаптивности
548
+
549
+ // Изменить настройки внешнего вида письма
550
+ await emailSettingsApi.setEmailLayoutSettings({
551
+ backgroundColor: '#ffffff',
552
+ width: { value: 600, dim: 'px' },
553
+ responsive: { shutdown: false, styles: true }
554
+ });
555
+ ```
556
+
557
+ #### Получение и установка стилей элементов контента:
558
+
559
+ ```ts
560
+ // Получить стили элементов контента
561
+ const contentStyles = await emailSettingsApi.getEmailContentStyles();
562
+ console.log(contentStyles.text); // стили текста
563
+ console.log(contentStyles.buttons); // стили кнопок
564
+ console.log(contentStyles.block); // стили блоков
565
+
566
+ // Изменить стили элементов контента
567
+ await emailSettingsApi.setEmailContentStyles({
568
+ text: { fontSize: 18, color: '#333333' },
569
+ buttons: {
570
+ backgroundColor: '#1890ff',
571
+ borderRadius: { all: 8 }
572
+ },
573
+ block: {
574
+ padding: { top: 20, right: 20, bottom: 20, left: 20 }
575
+ }
576
+ });
577
+ ```
578
+
579
+ > ⚠️ Методы для работы с layout settings (`getEmailLayoutSettings`, `setEmailLayoutSettings`) ждут инициализации письма из iframe перед возвратом или установкой данных, чтобы гарантировать актуальность информации. Методы для работы с content styles не требуют ожидания, так как стили хранятся в объекте письма.
580
+
581
+
582
+ ### MessageService
583
+
584
+ > Типизированная надстройка над `postMessage`, для обмена сообщениями между UI и sandbox.
585
+
586
+ ```ts
587
+ const msg = editor.use('MessageService');
588
+ msg.send('MY_EVENT', { value: 123 });
589
+
590
+ msg.addListener('MY_EVENT', (data) => console.log(data));
591
+ ```
592
+
593
+ Рекомендуется выносить события в константы и использовать типизацию:
594
+
595
+ ```ts
596
+ import { createIdentifier } from 'di';
597
+
598
+ export const Activate_Product_Settings = createIdentifier<
599
+ { groupId?: string; visualSettings: VisualSettings },
600
+ void,
601
+ void
602
+ >('Activate_Product_Settings');
603
+ ```
604
+
605
+
606
+ ### SandboxScriptApi
607
+
608
+ Позволяет динамически подключать JS и CSS в iframe, например, для загрузки сторонних библиотек. Также используется для загрузки скрипта, взаимодействующего с DOM письма:
609
+
610
+ ```ts
611
+ const sandboxApi = editor.use('SandboxScriptApi');
612
+
613
+ const style = sandboxApi.registerSandboxCSS('https://cdn.com/style.css');
614
+ const script = sandboxApi.registerSandboxScript('https://cdn.com/script.js');
615
+
616
+ // или по коду, если это допустимо CSP
617
+ sandboxApi.registerSandboxScriptCode('console.log("hi")');
618
+ ```
619
+
620
+ ⚠️ Использование `registerSandboxScriptCode` может нарушить политику безопасности (CSP). Лучше предпочитать загрузку внешних скриптов через `registerSandboxScript()`.
621
+
622
+
623
+ ---
624
+
625
+ ## 🧪 Плагины Sandbox
626
+
627
+ Работают внутри iframe для взаимодействия с DOM письма.
628
+
629
+ ```ts
630
+ import { App } from '@emailmaker/extensions-sandbox';
631
+ import type { IPlugin } from '@emailmaker/extensions-sandbox';
632
+
633
+ class MyPluginSandbox implements IPlugin {
634
+ init() {
635
+ const messageService = App.use('MessageService');
636
+
637
+ messageService.addListener('my-plugin:update', (data) => {
638
+ const element = document.querySelector('.my-block');
639
+ if (element) {
640
+ element.textContent = data.text;
641
+ App.use('SyncService').commit();
642
+ }
643
+ });
644
+ }
645
+
646
+ dispose() {}
647
+ }
648
+
649
+ App.registerPlugin('MyPluginSandbox', MyPluginSandbox);
650
+ ```
651
+
652
+ > ⚠️ Sandbox работает в изолированном iframe. React-компоненты здесь недоступны — только нативный DOM.
653
+
654
+
655
+ ### MessageService (внутри sandbox)
656
+
657
+ Аналогичен UI-версии. Позволяет слушать и отправлять сообщения.
658
+
659
+ ```ts
660
+ import { App } from '@emailmaker/extensions-sandbox';
661
+
662
+ const messageService = App.use('MessageService');
663
+
664
+ // Отправка в app
665
+ messageService.send('my-plugin:click', { elementId: '123' });
666
+
667
+ // Получение из app
668
+ messageService.addListener('my-plugin:update', (data) => {
669
+ console.log('Received:', data);
670
+ });
671
+ ```
672
+
673
+
674
+ ### SyncService
675
+
676
+ Для сохранения изменений из DOM в код письма:
677
+
678
+ ```ts
679
+ import { App } from '@emailmaker/extensions-sandbox';
680
+
681
+ const syncService = App.use('SyncService');
682
+
683
+ document.querySelector('.title').textContent = 'Updated';
684
+ syncService.commit();
685
+ ```
686
+
687
+ > ⚠️ Все несохранённые (незакоммиченные) изменения могут быть утеряны при следующем рендере.
688
+
689
+
690
+ ---
691
+
692
+ ## 🧱 Регистрация UI компонентов
693
+
694
+ `ComponentRegistry` нужен, когда вы хотите зарегистрировать React-компонент и потом передавать его в API по идентификатору:
695
+
696
+ ```ts
697
+ import { ComponentRegistry } from '@emailmaker/extensions-react';
698
+
699
+ ComponentRegistry.add('MyPanel', MyReactPanel);
700
+
701
+ ComponentRegistry.override('MyPanel', (Base) => (props) => (
702
+ <div className="bordered"><Base {...props} /></div>
703
+ ));
704
+ ```
705
+
706
+ ## Когда использовать
707
+
708
+ - если хотите просто показать React UI, чаще всего удобнее передать JSX напрямую
709
+ - если нужен `ComponentId`, используйте `ComponentRegistry`
710
+ - если UI без React, используйте `DomComponentRegistry`
711
+
712
+ Пример с JSX:
713
+
714
+ ```ts
715
+ import { SettingsPanelApi } from '@emailmaker/extensions-app';
716
+ import '@emailmaker/extensions-react';
717
+
718
+ settingsPanel.showSettingsPanel({
719
+ content: <MyPanel />,
720
+ });
721
+ ```
722
+
723
+
724
+ ---
725
+
726
+ ## 🎨 UI Kit
727
+
728
+ ```tsx
729
+ import { EmIcons, ColorPicker } from "@emailmaker/ui-kit";
730
+
731
+ <ColorPicker
732
+ value={settings.bgColor}
733
+ onChange={(v) => onChange({ ...settings, bgColor: v })}
734
+ />
735
+ ```
736
+
737
+
738
+ ---
739
+
740
+ # ⚛️ `extensions-react`
741
+
742
+ `@emailmaker/extensions-react` нужен для React-плагинов.
743
+
744
+ ## Что в нем есть
745
+
746
+ - `ComponentRegistry`
747
+ - `createComponentIdentifier()`
748
+ - поддержка React-компонентов в API
749
+
750
+ ## Что можно делать
751
+
752
+ После установки пакета вы можете передавать JSX прямо в API:
753
+
754
+ ```ts
755
+ import { SettingsPanelApi } from '@emailmaker/extensions-app';
756
+ import '@emailmaker/extensions-react';
757
+
758
+ settingsPanel.showSettingsPanel({
759
+ content: <MyPanel />,
760
+ });
761
+ ```
762
+
763
+ ## Когда использовать JSX
764
+
765
+ Прямой JSX — лучший путь по умолчанию для React-плагина:
766
+
767
+ ```ts
768
+ settingsPanel.showSettingsPanel({
769
+ content: <MyPanel value={state} />,
770
+ });
771
+ ```
772
+
773
+ Он хорош, когда:
774
+
775
+ - не нужен стабильный `ComponentId`
776
+ - не нужен `override()`
777
+ - UI используется локально в одном месте
778
+
779
+ ## Когда использовать `ComponentRegistry`
780
+
781
+ `ComponentRegistry` нужен, если:
782
+
783
+ - компонент должен быть доступен по `ComponentId`
784
+ - нужна возможность `override()`
785
+ - компонент используется в нескольких местах через `{ type, props }`
786
+ - нужен стабильный идентификатор для контракта между частями плагина
787
+
788
+ Пример:
789
+
790
+ ```ts
791
+ import { createComponentIdentifier } from '@emailmaker/extensions-app';
792
+ import { ComponentRegistry } from '@emailmaker/extensions-react';
793
+
794
+ const PanelId = createComponentIdentifier<{ value: string }>('MyPanel');
795
+
796
+ ComponentRegistry.add(PanelId, MyPanel);
797
+
798
+ settingsPanel.showSettingsPanel({
799
+ content: {
800
+ type: PanelId,
801
+ props: { value: 'hello' },
802
+ },
803
+ });
804
+ ```
805
+
806
+ ## Короткое правило
807
+
808
+ - для React-плагина ставьте `@emailmaker/extensions-react`
809
+ - для обычного React UI чаще всего достаточно JSX
810
+ - если нужен идентификатор компонента, используйте `ComponentRegistry`
811
+
812
+
813
+ ---
814
+
815
+ ## 📦 Расширение типов
816
+
817
+ ### PluginTypeMap
818
+
819
+ `PluginTypeMap` — **опциональный** механизм для связи между плагинами по строковому имени. Позволяет использовать `editor.use('PluginName')` с автодополнением.
820
+
821
+ **Когда нужен:**
822
+ - Плагин A обращается к плагину B через `editor.use('PluginB')`
823
+ - URL-режимы `globals` / `async-globals`, если плагин подключается через descriptor с `resolve: 'registry'`
824
+
825
+ **Когда не нужен:**
826
+ - Плагин подключается напрямую через класс: `plugins: [MyPlugin]`
827
+ - Типизация через `editor.use(MyPlugin)` (прямая ссылка на класс)
828
+
829
+ ```ts
830
+ declare module "@emailmaker/emailmaker" {
831
+ interface PluginTypeMap {
832
+ TestPlugin: typeof TestPlugin;
833
+ }
834
+ }
835
+ ```
836
+
837
+ `PluginTypeMap` наследует все базовые плагины из `BasePluginTypeMap`, поэтому доступны автодополнения для встроенных API (ElementsApi, MessageService, ModalApi и т.д.).
838
+
839
+ > 💡 Рекомендуем `editor.use(MyPlugin)` с типизацией через класс напрямую. `PluginTypeMap` нужен для `editor.use('Name')` между плагинами и для typed descriptor-ов вида `{ type: 'umd', url, name, resolve: 'registry' }`.
840
+
841
+ ### Опции плагина
842
+
843
+ Конструктор плагина (класс или фабрика) принимает `(context, options?)`.
844
+ Для типизации используйте `ExtractPluginOptions`:
845
+
846
+ ```ts
847
+ import type { ExtractPluginOptions } from '@emailmaker/emailmaker';
848
+
849
+ // Опции извлекаются из конструктора
850
+ type MyOpts = ExtractPluginOptions<typeof MyPlugin>;
851
+ ```
852
+
853
+ Опции передаются через кортеж в `plugins`:
854
+
855
+ ```ts
856
+ emailmaker.init({
857
+ plugins: [
858
+ [MyPlugin, { theme: 'dark' }]
859
+ ]
860
+ });
861
+ ```
862
+
863
+ > При множественной инициализации действует принцип «first-write wins» — опции фиксируются при первом создании экземпляра.
864
+
865
+ CLI-шаблон уже собирает типы отдельно (`npm run build:types`) и публикует `dist/index.d.ts`, поэтому поддержку TypeScript стоит считать частью любого release target, а не только npm/ESM сценария.
866
+
867
+ ### Расширение Config
868
+
869
+ Публикуйте `.d.ts` вместе с плагином для расширения конфигурации редактора:
870
+
871
+ ```ts
872
+ declare module "@emailmaker/emailmaker" {
873
+ interface Config {
874
+ productBlock?: {
875
+ enabled?: boolean;
876
+ groups: Group[];
877
+ };
878
+ }
879
+ }
880
+ ```
881
+
882
+ ### React-компоненты в API
883
+
884
+ Если в проекте подключен `@emailmaker/extensions-react`, в API можно передавать React-компоненты и JSX напрямую.
885
+
886
+ Пример:
887
+
888
+ ```ts
889
+ import { SettingsPanelApi } from '@emailmaker/extensions-app';
890
+ import '@emailmaker/extensions-react';
891
+
892
+ settingsPanel.showSettingsPanel({
893
+ content: <MyPanel />,
894
+ });
895
+ ```
896
+
897
+ > Если вы пишете React-плагин, просто установите `@emailmaker/extensions-react`.
898
+
899
+
900
+ ---
901
+
902
+ # 🧰 CLI
903
+
904
+ `@emailmaker/cli` — публичный генератор проектов для внешних плагинов и demo-стендов.
905
+
906
+ ## Что умеет CLI
907
+
908
+ - интерактивный wizard
909
+ - неинтерактивный запуск через аргументы командной строки
910
+ - генерация plugin-проекта
911
+ - генерация stand / stand-closed сценариев
912
+ - шаблоны под `vite` и `webpack`
913
+ - выбор target-а для внешнего плагина: `npm-пакет`, `встраиваемый bundle`, `advanced`
914
+
915
+ ## Основные режимы
916
+
917
+ ### `plugin`
918
+
919
+ Генерирует внешний плагин с:
920
+
921
+ - app-частью
922
+ - sandbox-частью
923
+ - local debug host
924
+ - базовой сборкой и scripts
925
+
926
+ ### `stand`
927
+
928
+ Генерирует тестовый стенд с API и OAuth.
929
+
930
+ ### `stand-closed`
931
+
932
+ Генерирует стенд для замкнутого контура (`standaloneKey`).
933
+
934
+ ## Preset-ы для plugin
935
+
936
+ ### `minimal`
937
+
938
+ Минимальный каркас:
939
+
940
+ - `pluginRegistry`
941
+ - базовая app/sandbox связка
942
+ - без React UI примера
943
+
944
+ ### `advanced`
945
+
946
+ Расширенный пример:
947
+
948
+ - React UI
949
+ - `ComponentRegistry` из `@emailmaker/extensions-react`
950
+ - пример регистрации panel/icon
951
+ - пример `SettingsPanelApi` и `MessageService`
952
+
953
+ ## Пример запуска
954
+
955
+ ```bash
956
+ npx @emailmaker/cli my-plugin --preset advanced --bundler vite --target npm-package --output-dir ./my-plugin
957
+ ```
958
+
959
+ ## Когда использовать CLI
960
+
961
+ CLI нужен, если вы хотите:
962
+
963
+ - быстро стартовать новый внешний плагин
964
+ - не собирать вручную `vite`/`webpack` конфиг
965
+ - сразу получить готовую структуру проекта
966
+ - получить рабочий пример app, sandbox и локальной отладки
967
+
968
+
969
+ ---
970
+
971
+ ## 🧯 Troubleshooting
972
+
973
+ ### `dist` неожиданно толстый
974
+
975
+ Проверьте сначала:
976
+
977
+ - что release target выбран как `npm-пакет` или `встраиваемый bundle`, а не `advanced`
978
+ - что release config использует `PluginDev(...)`, а не ручной `externals`
979
+ - что в bundle не попали `react`, `antd` или runtime subpath-ы как обычные модули
980
+
981
+ Частая причина: release config собирается без корректного externals/alias режима.
982
+
983
+ ### Плагин работает в `start`, но ломается в release
984
+
985
+ Обычно это значит, что dev host использует `PluginDev({ externals: false })`, а release target уже требует runtime contract.
986
+
987
+ Проверьте:
988
+
989
+ - target сборки в CLI / config
990
+ - release config для `src/index.ts`
991
+ - способ загрузки плагина в host (`npm-пакет` vs `встраиваемый bundle`)
992
+
993
+ ### Browser target не загружается
994
+
995
+ Проверьте:
996
+
997
+ - что host заранее загрузил runtime globals
998
+ - что release bundle собран под target `встраиваемый bundle`
999
+ - что sandbox и main bundle публикуются по ожидаемым URL
1000
+
1001
+ ### Duplicate React / hooks error
1002
+
1003
+ Почти всегда это значит, что React попал в bundle плагина вместо runtime externals.
1004
+
1005
+ Проверьте:
1006
+
1007
+ - используется ли `PluginDev` в release config
1008
+ - не отключён ли externals вручную
1009
+ - нет ли дополнительных alias/resolve правил, которые обходят runtime contract
1010
+
1011
+ ### Когда использовать `advanced`
1012
+
1013
+ Только если host уже требует Webpack async/globals compatibility.
1014
+
1015
+ Для обычного внешнего плагина:
1016
+
1017
+ - `npm-пакет` — основной путь
1018
+ - `встраиваемый bundle` — для script/UMD загрузки
1019
+ - `advanced` — только для совместимости, когда вы точно знаете контракт host
1020
+
1021
+
1022
+ ---
1023
+
1024
+ ## ❓ FAQ / Типичные ошибки
1025
+
1026
+ - **«Invalid hook call» или два React на странице**
1027
+ > Для обычного плагина используйте стандартные импорты `react` / `antd` и собирайте проект через `PluginDev` с `alias: true` (по умолчанию). Не смешивайте обычные импорты с прямыми импортами из runtime в одном plugin UI.
1028
+
1029
+ - **Externals не работают в dev server**
1030
+ > В serve-режиме externals должны быть отключены: `PluginDev({ externals: false })`. Зависимости резолвятся из runtime через alias.
1031
+
1032
+ - **ESM output не грузится через `<script>`**
1033
+ > ESM-сборка содержит `import` и не работает как обычный скрипт. Используйте `<script type="module">` или соберите плагин в режиме `global` / `iife`.
1034
+
1035
+ - **Global-плагин: «Cannot read property of undefined»**
1036
+ > Runtime должен быть загружен до плагина. Подключайте плагин через ленивый импорт `() => import(…)` или декларативно `{ type: 'global', url: '…' }`.
1037
+
1038
+ - **Не загружаются JS/CSS / iframe не отображается**
1039
+ > Проверьте `publicPath` и подключение основного плагина (`VitePlugin` / `WebpackPlugin`). В serve-режиме он обслуживает статику из `node_modules/`.
1040
+
1041
+ - **Панель или компонент не отображается**
1042
+ > Убедитесь в регистрации через `ComponentRegistry`.
1043
+
1044
+ - **Событие не обрабатывается**
1045
+ > Проверьте подписку на событие в `MessageService`.
1046
+
1047
+ - **Изменения в iframe теряются**
1048
+ > Не забывайте вызывать `syncService.commit()`.
1049
+
1050
+ ---
1051
+
1052
+ ### 🔄 Миграция с предыдущих версий
1053
+
1054
+ **С `externals.json` на `PluginDev`:**
1055
+
1056
+ Старый подход с `externals.json` продолжает работать — файл генерируется для обратной совместимости. Но рекомендуем перейти на `PluginDev`: удалите загрузку `externals.json` из конфига и подключите плагин — он настроит externals и alias автоматически.
1057
+
1058
+ **С `pluginRegistry.add` на прямой класс:**
1059
+
1060
+ До:
1061
+ ```typescript
1062
+ pluginRegistry.add('MyPlugin', MyPlugin);
1063
+ // init: plugins: ['MyPlugin']
1064
+ ```
1065
+
1066
+ После (если не нужен `editor.use('MyPlugin')` между плагинами):
1067
+ ```typescript
1068
+ // init: plugins: [MyPlugin]
1069
+ ```
1070
+
1071
+
1072
+ ---
1073
+
1074
+ ## 🔗 Полезные ссылки
1075
+
1076
+ - [Webpack](https://github.com/webpack/webpack)
1077
+ - [Vite](https://vitejs.dev/)
1078
+ - [React](https://github.com/facebook/react)
1079
+ - [TypeScript](https://github.com/microsoft/TypeScript)
1080
+ - [Ant Design](https://github.com/ant-design/ant-design)
1081
+ - [Пример плагина](https://github.com/emailmaker/simple_plugin)
1082
+