@reformer/builder-plugin-api 1.0.0-beta.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/dist/index.d.ts +147 -0
  4. package/dist/index.js +76 -0
  5. package/dist/internal.d.ts +78 -0
  6. package/dist/internal.js +289 -0
  7. package/dist/permissions-BWlsUVPs.js +551 -0
  8. package/dist/plugin/bundled-modules.d.ts +25 -0
  9. package/dist/plugin/layout.d.ts +27 -0
  10. package/dist/plugin/manifest-parser.d.ts +54 -0
  11. package/dist/plugin/manifest.d.ts +320 -0
  12. package/dist/plugin/messages-bundle.d.ts +24 -0
  13. package/dist/plugin/permissions.d.ts +48 -0
  14. package/dist/plugin/plugin-exports.d.ts +10 -0
  15. package/dist/plugin/runtime-modules.d.ts +20 -0
  16. package/dist/plugin/storage.d.ts +70 -0
  17. package/dist/plugin/types.d.ts +136 -0
  18. package/dist/primitives/capability.d.ts +72 -0
  19. package/dist/primitives/command.d.ts +262 -0
  20. package/dist/primitives/disposable.d.ts +23 -0
  21. package/dist/primitives/event.d.ts +38 -0
  22. package/dist/primitives/extension-point.d.ts +53 -0
  23. package/dist/primitives/module-path.d.ts +21 -0
  24. package/dist/primitives/resource.d.ts +159 -0
  25. package/dist/primitives/semver.d.ts +93 -0
  26. package/dist/primitives/service.d.ts +68 -0
  27. package/dist/primitives/when-context.d.ts +69 -0
  28. package/dist/primitives/when-expr.d.ts +177 -0
  29. package/dist/resource-clipboard-Bdk_Is5N.js +381 -0
  30. package/dist/runtime-modules-CiUDFMIn.js +597 -0
  31. package/dist/services/context-keys.d.ts +52 -0
  32. package/dist/services/diagnostics/fixes.d.ts +40 -0
  33. package/dist/services/diagnostics/service.d.ts +34 -0
  34. package/dist/services/diagnostics/types.d.ts +148 -0
  35. package/dist/services/document-models.d.ts +20 -0
  36. package/dist/services/documents.d.ts +94 -0
  37. package/dist/services/host-messages.d.ts +10 -0
  38. package/dist/services/i18n.d.ts +43 -0
  39. package/dist/services/modules.d.ts +49 -0
  40. package/dist/services/notifications.d.ts +56 -0
  41. package/dist/services/plugin-settings.d.ts +10 -0
  42. package/dist/services/plugins-catalog.d.ts +58 -0
  43. package/dist/services/preview.d.ts +163 -0
  44. package/dist/services/prompt.d.ts +111 -0
  45. package/dist/services/resource-clipboard.d.ts +28 -0
  46. package/dist/services/selection.d.ts +46 -0
  47. package/dist/services/settings.d.ts +44 -0
  48. package/dist/services/theme.d.ts +15 -0
  49. package/dist/services/validation/types.d.ts +98 -0
  50. package/dist/services/workspace-files.d.ts +84 -0
  51. package/dist/services/workspace-resources.d.ts +41 -0
  52. package/dist/services/workspace-save.d.ts +18 -0
  53. package/dist/tooling.d.ts +28 -0
  54. package/dist/tooling.js +35 -0
  55. package/dist/ui/contributions/decorations.d.ts +55 -0
  56. package/dist/ui/contributions/editors.d.ts +58 -0
  57. package/dist/ui/contributions/plugin-settings.d.ts +54 -0
  58. package/dist/ui/keyboard/keybinding-rules.d.ts +48 -0
  59. package/dist/ui/keyboard/keybindings.d.ts +55 -0
  60. package/dist/ui/keyboard/keymap.d.ts +81 -0
  61. package/dist/ui/keyboard/scope.d.ts +38 -0
  62. package/dist/ui/menu/editor-menu.d.ts +25 -0
  63. package/dist/ui/menu/menu.d.ts +228 -0
  64. package/dist/ui/menu/palette.d.ts +36 -0
  65. package/dist/ui/menu/resource-menu.d.ts +45 -0
  66. package/dist/ui/slots.d.ts +113 -0
  67. package/dist/ui/useActiveDocument.d.ts +5 -0
  68. package/dist/ui/useLocale.d.ts +9 -0
  69. package/dist/ui/useTranslate.d.ts +4 -0
  70. package/dist/workspace/document.d.ts +42 -0
  71. package/dist/workspace/model/editor-view-states.d.ts +29 -0
  72. package/dist/workspace/model/model-document.d.ts +86 -0
  73. package/dist/workspace/model/provider.d.ts +140 -0
  74. package/dist/workspace/model/text-editor-focus.d.ts +22 -0
  75. package/dist/workspace/resource-names.d.ts +92 -0
  76. package/dist/workspace/write-options.d.ts +36 -0
  77. package/package.json +69 -0
@@ -0,0 +1,320 @@
1
+ import { CapabilityDeclaration, CapabilityRequirement } from '../primitives/capability.js';
2
+ import { PluginPermission } from './permissions.js';
3
+ /** Имя файла манифеста внутри каталога плагина. */
4
+ export declare const PLUGIN_MANIFEST_FILE = "manifest.json";
5
+ /**
6
+ * Версия API плагинов, которую даёт эта сборка оболочки.
7
+ *
8
+ * Константа, а не мажор числом: с ней `apiVersion: "^1.2"` наконец значит то, что написано, —
9
+ * прежний разбор доставал из диапазона первое число и не отличал `^1` от `1.5`. Растить её
10
+ * обязан тот, кто меняет `@reformer/builder-plugin-api`: минор — на добавление имени, мажор — на удаление или смену
11
+ * смысла. Политики совместимости между мажорами как не было, так и нет (plugin-and-shell.md).
12
+ */
13
+ export declare const BUILDER_API_VERSION = "1.0.0";
14
+ /**
15
+ * Откуда плагин взялся. Разбор манифеста спрашивают об этом ЗАРАНЕЕ, а не выводят потом.
16
+ *
17
+ * Две поставки различаются не «происхождением вообще», а двумя проверками, которые нельзя
18
+ * сделать одинаковыми. У плагина ПРОЕКТА идентификатор обязан совпасть с именем каталога —
19
+ * каталог единственное, что видно до чтения манифеста, — и точка входа обязана быть, иначе
20
+ * грузить нечего. У ВСТРОЕННОГО каталога нет вовсе (он лежит в бандле оболочки, а имя его
21
+ * папки — `ai` против идентификатора `reformer.ai`), и точки входа тоже нет: его код уже
22
+ * здесь. Зато у него есть то, чего не бывает у плагина проекта, — способ доставки
23
+ * ({@link BuiltinDelivery}).
24
+ */
25
+ export type PluginSource = {
26
+ readonly kind: 'builtin';
27
+ } | {
28
+ readonly kind: 'project';
29
+ readonly dir: string;
30
+ };
31
+ /**
32
+ * Как встроенный плагин приезжает в браузер.
33
+ *
34
+ * Деление про СБОРКУ, а не про поведение: оба набора встают до первой отрисовки. Смысл в том,
35
+ * что иначе код всех плагинов лежит внутри entry одним файлом, а отдельный файл даёт только
36
+ * динамический импорт (`manualChunks` измерен и отвергнут — см. `vite.config.ts`).
37
+ *
38
+ * `reason` заполняется у СТАТИЧЕСКИХ и только у них: ленивость — умолчание, объяснять надо
39
+ * отступление от него. Живёт причина здесь, а не комментарием у записи состава, потому что
40
+ * относится к плагину, а не к карте: карта может смениться, а довод останется тем же.
41
+ */
42
+ export interface BuiltinDelivery {
43
+ readonly loading: 'eager' | 'lazy';
44
+ /** Почему этот плагин не может приехать своим файлом. Обязателен при `eager`. */
45
+ readonly reason?: string;
46
+ }
47
+ /**
48
+ * Общее у манифестов обеих поставок.
49
+ *
50
+ * `name` и `version` необязательны в файле и получают умолчания: они нужны списку плагинов,
51
+ * а не механике, и требовать их значило бы отвергать рабочий плагин из-за подписи. `id`
52
+ * и `apiVersion` умолчаний не имеют — у них нет осмысленного «по умолчанию».
53
+ */
54
+ export interface PluginManifestBase {
55
+ readonly id: string;
56
+ readonly name: string;
57
+ readonly version: string;
58
+ /** Диапазон API, объявленный плагином, как он написан в файле: `^1`, `1.2`, `~1.0.0`. */
59
+ readonly apiVersion: string;
60
+ /** Вклады, объявленные ДЕКЛАРАТИВНО — то есть видимые до того, как плагин включён. */
61
+ readonly contributes?: PluginContributes;
62
+ /**
63
+ * Возможности, которые плагин ДАЁТ другим: `[{ "id": "reformer.kit.catalog", "version": "1.0.0" }]`.
64
+ *
65
+ * Объявление, а не регистрация: слот в реестре служб плагин занимает сам, в `activate`.
66
+ * Расхождение между объявленным и занятым ловит рантайм (`./registry`) — иначе резолвер
67
+ * верил бы манифесту, а реестр молчал бы, и «возможность есть» означало бы «написано,
68
+ * что есть».
69
+ */
70
+ readonly provides?: readonly CapabilityDeclaration[];
71
+ /**
72
+ * Возможности, которые плагину НУЖНЫ, — двумя списками.
73
+ *
74
+ * Деление на обязательные и необязательные не косметическое, оно про разные исходы.
75
+ * Невыполненное ОБЯЗАТЕЛЬНОЕ требование — отказ включения ДО того, как исполнится код
76
+ * плагина (`./catalog`, `requires-unsatisfied`): плагин, которому нечем работать, не должен
77
+ * получать шанс упасть на середине `activate` и оставить половину вкладов.
78
+ * Невыполненное НЕОБЯЗАТЕЛЬНОЕ — названная деградация, ровно по принципу «необязательный
79
+ * член контракта = названная деградация, а не поломка»: плагин включается и работает
80
+ * без этой возможности, спрашивая её через `ctx.capabilities.get`.
81
+ */
82
+ readonly requires?: PluginRequirements;
83
+ /**
84
+ * Права, которые плагин просит: `["workspace.save"]`.
85
+ *
86
+ * Не декорация и не намерение — ключ к привилегированной службе. Оболочка отдаёт такую
87
+ * службу только тому, кто её здесь назвал И кому человек это подтвердил; не назвавший
88
+ * получает `undefined` на месте объекта. Поэтому список закрыт ({@link PluginPermission}):
89
+ * право, за которым не стоит запертой двери, было бы ровно тем ложным ощущением границы,
90
+ * из-за которого поля `permissions` не было до появления первой такой двери.
91
+ */
92
+ readonly permissions?: readonly PluginPermission[];
93
+ /**
94
+ * Совместимость с ПРИЛОЖЕНИЕМ: `{ "builder": ">=2.1" }`.
95
+ *
96
+ * Отдельная ось от {@link PluginManifestBase.apiVersion}, а не её уточнение. `apiVersion`
97
+ * отвечает «какие имена доступны плагину» — это версия контракта; `compatibility.builder`
98
+ * отвечает «какое приложение их подаёт». Расходятся они по построению: контракт растёт,
99
+ * когда меняется поверхность плагина, а билдер выпускается по своим причинам, и плагину
100
+ * бывает нужно сказать «мне нужна сборка, где это уже чинено», не требуя нового контракта.
101
+ *
102
+ * Поля не было, пока у оболочки не появилось своей версии в рантайме: проверять его было
103
+ * нечем, а объявление, которое ничего не принуждает, — то же ложное ощущение границы,
104
+ * из-за которого до первой запертой службы не было `permissions`. Теперь несовпадение —
105
+ * отказ загрузки (`builder-version`), а у того, кто своей версии не знает (CLI автора
106
+ * плагина), проверяется форма диапазона.
107
+ */
108
+ readonly compatibility?: PluginCompatibility;
109
+ }
110
+ /** Совместимость с приложением. Один член; растёт вместе с тем, у чего появится версия. */
111
+ export interface PluginCompatibility {
112
+ /** Диапазон версий билдера: `^2`, `>=2.1`, `~2.1.0`. */
113
+ readonly builder: string;
114
+ }
115
+ /** Манифест плагина каталога проекта: у него есть каталог и точка входа. */
116
+ export interface ProjectPluginManifest extends PluginManifestBase {
117
+ readonly source: {
118
+ readonly kind: 'project';
119
+ readonly dir: string;
120
+ };
121
+ /** Точка входа ВНУТРИ каталога плагина, нормализованная: `main.js`, `dist/main.js`. */
122
+ readonly main: string;
123
+ /**
124
+ * Своя таблица стилей. Отсутствие поля — рекомендуемый путь: плагин пользуется классами
125
+ * оболочки и токенами кита и выглядит родным бесплатно.
126
+ */
127
+ readonly styles?: PluginStyles;
128
+ }
129
+ /**
130
+ * Манифест встроенного плагина: точки входа нет, зато объявлен способ доставки.
131
+ *
132
+ * Своей таблицы стилей у встроенного не бывает и быть не может: его CSS собирается вместе
133
+ * с оболочкой, и изолировать его было бы нечего и не от чего.
134
+ */
135
+ export interface BuiltinPluginManifest extends PluginManifestBase {
136
+ readonly source: {
137
+ readonly kind: 'builtin';
138
+ };
139
+ readonly builtin: BuiltinDelivery;
140
+ }
141
+ /**
142
+ * Манифест исходников плагина: то, что лежит в репозитории автора ДО сборки.
143
+ *
144
+ * Не поставка — оболочка такого манифеста не видит, — поэтому и не вариант {@link PluginManifest}.
145
+ * Форма та же, что у плагина каталога, без одного: каталога ещё нет, и `source` описывать нечему.
146
+ * `main` указывает на исходник (`src/main.ts`); собранный манифест получит `main.js`.
147
+ * Разбор — `parsePluginSourceManifest`.
148
+ */
149
+ export interface PluginSourceManifest extends PluginManifestBase {
150
+ readonly main: string;
151
+ readonly styles?: PluginStyles;
152
+ }
153
+ /**
154
+ * Разобранный манифест — одной из двух поставок.
155
+ *
156
+ * Объединение размечено {@link PluginSource}, а не двумя необязательными полями: «точка входа
157
+ * есть, но у встроенного её не бывает» пришлось бы проверять в загрузчике на каждом обращении,
158
+ * и отсутствие `main` у того, кого грузят из каталога, стало бы не ошибкой разбора,
159
+ * а исключением где-то посередине загрузки.
160
+ */
161
+ export type PluginManifest = ProjectPluginManifest | BuiltinPluginManifest;
162
+ /** Требования плагина. Оба списка есть всегда — пустые, если в манифесте их не написали. */
163
+ export interface PluginRequirements {
164
+ readonly required: readonly CapabilityRequirement[];
165
+ readonly optional: readonly CapabilityRequirement[];
166
+ }
167
+ /**
168
+ * Декларативные вклады манифеста — то есть видимые ДО того, как плагин включён.
169
+ *
170
+ * Клавиши попали сюда по проверяемой причине, а не «для симметрии с VS Code»: сочетание,
171
+ * объявленное КОДОМ, появляется в приложении только после активации плагина. Значит до
172
+ * включения таблица клавиш о нём не знает, и переназначить его нельзя — а человеку это нужно
173
+ * ровно тогда, когда новый плагин занял привычную ему клавишу.
174
+ *
175
+ * Словари попали сюда по причине жёстче: без них плагину каталога негде взять СВОЙ текст
176
+ * вовсе. Заголовок команды разрешается словарём её владельца (`primitives/command`, поле
177
+ * `titleKey`), сервиса i18n в `PluginContext` нет и не будет (вклад в словарь не снимается
178
+ * вместе с плагином, значит его подпиской быть не может), а словари встроенных вносит
179
+ * композиция — кодом, которого у внешнего плагина не существует. Итог без этого поля:
180
+ * КАЖДАЯ команда плагина каталога показана в палитре и меню маркером промаха
181
+ * `⟦mycode.command.format⟧`.
182
+ */
183
+ export interface PluginContributes {
184
+ readonly keybindings?: readonly DeclaredKeybinding[];
185
+ /**
186
+ * Словари: локаль → путь к JSON внутри каталога плагина, например
187
+ * `{ "ru": "locales/ru.json", "en": "locales/en.json" }`.
188
+ *
189
+ * Путь, а не сам словарь. Манифесты читаются у ВСЕХ найденных плагинов на каждом обходе
190
+ * каталога, включая выключенные, и встроенный текст превратил бы обход в чтение всех
191
+ * переводов всех плагинов проекта. Файл читается один раз и только у того, кого включили
192
+ * (`./loader`).
193
+ */
194
+ readonly messages?: Readonly<Record<string, string>>;
195
+ }
196
+ /**
197
+ * Сочетание, объявленное в манифесте.
198
+ *
199
+ * `command` — строка, и плагин вправе назвать команду, которой сейчас нет: она появится
200
+ * при активации. Правило без команды просто не срабатывает — это обычное состояние
201
+ * выключенного плагина, а не поломка.
202
+ */
203
+ export interface DeclaredKeybinding {
204
+ readonly command: string;
205
+ /** Сочетание или аккорд: `mod+alt+i`, `mod+k mod+i`. */
206
+ readonly key: string;
207
+ /** Условие применимости; синтаксис — `primitives/when-expr`. */
208
+ readonly when?: string;
209
+ /** Аргументы вызова: у клавиши их нет, поэтому объявить их можно только здесь. */
210
+ readonly args?: unknown;
211
+ readonly allowInEditable?: boolean;
212
+ }
213
+ /**
214
+ * Объявление своей таблицы стилей.
215
+ *
216
+ * `isolation` необязателен и умеет ровно одно значение. Поле существует не ради выбора,
217
+ * а ради читаемости манифеста: `"isolation": "scoped"` рядом с файлом говорит автору плагина,
218
+ * что его CSS ограничат, — и он не будет искать причину, почему `body { margin: 0 }`
219
+ * не подействовал на весь документ. Второго значения нет и не планируется: неизолированный
220
+ * чужой CSS перекрашивает оболочку, и это не режим, а поломка.
221
+ */
222
+ export interface PluginStyles {
223
+ /** Путь к CSS ВНУТРИ каталога плагина, нормализованный. */
224
+ readonly file: string;
225
+ readonly isolation: 'scoped';
226
+ }
227
+ /**
228
+ * Почему плагин не работает. Код — для интерфейса и тестов, `message` — для человека.
229
+ *
230
+ * Набор плоский и общий на весь путь «нашли → разобрали → загрузили → включили», потому что
231
+ * показывается он в одном месте — строке списка плагинов, — и различать там «отказ разбора»
232
+ * от «отказа загрузки» человеку незачем: ему нужно знать, что чинить.
233
+ */
234
+ export type PluginProblemCode =
235
+ /** В каталоге плагина нет `manifest.json`. */
236
+ 'manifest-missing'
237
+ /** Манифест не читается или не разбирается как JSON. */
238
+ | 'manifest-unreadable'
239
+ /** Манифест разобран, но поле отсутствует или не того вида. */
240
+ | 'manifest-invalid'
241
+ /** `id` в манифесте не совпадает с именем каталога. */
242
+ | 'id-mismatch'
243
+ /** `apiVersion` не покрывает версию оболочки: плагин написан против другой. */
244
+ | 'api-version'
245
+ /**
246
+ * `compatibility.builder` не покрывает версию ПРИЛОЖЕНИЯ.
247
+ *
248
+ * Отдельный код, а не `api-version`: контракт может совпадать полностью, и чинится это
249
+ * иначе — обновлением билдера, а не переписыванием плагина под другой контракт.
250
+ */
251
+ | 'builder-version'
252
+ /**
253
+ * Обязательное требование `requires.required` не выполнено ничем из доступного.
254
+ *
255
+ * Проверяется ДО загрузки кода (`./catalog`), поэтому плагин не получает шанса упасть
256
+ * на середине `activate`. Строка в списке плагинов остаётся — с этой причиной, как
257
+ * у `styles-invalid` и `messages-invalid`.
258
+ */
259
+ | 'requires-unsatisfied'
260
+ /**
261
+ * Плагин объявил в `provides` возможность, которую к концу `activate` не зарегистрировал.
262
+ *
263
+ * Отдельный код, а не `activate-failed`: `activate` тут как раз НЕ бросал. Отличать их
264
+ * нужно тому, кто чинит плагин, — это ошибка в самом плагине, а не в его окружении.
265
+ */
266
+ | 'provides-unregistered'
267
+ /** Файла точки входа нет среди файлов плагина. */
268
+ | 'entry-missing'
269
+ /** В каталоге плагина слишком много файлов — это не плагин, а чужое дерево. */
270
+ | 'too-many-files'
271
+ /** Источник не разрешает исполнять свой код (`capabilities.executesCode`). */
272
+ | 'source-forbids-code'
273
+ /** Транспиляция, линковка или исполнение модуля отказали. */
274
+ | 'code-failed'
275
+ /** Точка входа экспортировала не плагин. */
276
+ | 'not-a-plugin'
277
+ /** Идентификатор уже занят другим плагином — встроенным или соседним по каталогу. */
278
+ | 'id-taken'
279
+ /** `activate` бросил. Плагин выключен и показан — автоповтора нет. */
280
+ | 'activate-failed'
281
+ /** Объявленная таблица стилей не разбирается или не изолируется (см. `./styles`). */
282
+ | 'styles-invalid'
283
+ /** Объявленный файл словаря не читается или это не плоский объект «ключ → строка». */
284
+ | 'messages-invalid'
285
+ /**
286
+ * Право, объявленное манифестом, человек не подтвердил.
287
+ *
288
+ * Проверяется ДО загрузки кода, как и требования: плагин, которому откажут в его главной
289
+ * службе, не должен исполниться наполовину. Отказ не гасит строку в списке — он её объясняет.
290
+ */
291
+ | 'permissions-denied';
292
+ /** Отказ как данные. Исключением он не бывает нигде: испорченный каталог — не авария. */
293
+ export interface PluginProblem {
294
+ readonly code: PluginProblemCode;
295
+ readonly message: string;
296
+ /** Файл, к которому отнесён отказ, если он известен. Путь внутри каталога плагина. */
297
+ readonly file?: string;
298
+ /** Исходное исключение — для консоли, не для показа. */
299
+ readonly cause?: unknown;
300
+ }
301
+ /**
302
+ * Манифест той поставки, которую назвали разбору.
303
+ *
304
+ * Нужен затем, чтобы загрузчик каталога получал манифест С точкой входа, а не объединение,
305
+ * у которого её может не быть: спросив разбор про каталог проекта, он спросил про плагин,
306
+ * у которого `main` есть по определению, и проверять это второй раз ему незачем.
307
+ */
308
+ export type ManifestOf<S extends PluginSource> = Extract<PluginManifest, {
309
+ readonly source: {
310
+ readonly kind: S['kind'];
311
+ };
312
+ }>;
313
+ /** Результат разбора: либо манифест, либо причина, по которой его нет. */
314
+ export type ManifestParseResult<M extends PluginManifestBase = PluginManifest> = {
315
+ readonly ok: true;
316
+ readonly manifest: M;
317
+ } | {
318
+ readonly ok: false;
319
+ readonly problem: PluginProblem;
320
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Файл словаря плагина: `locales/ru.json` и соседи.
3
+ *
4
+ * Один разбор на загрузчик оболочки и `reformer-plugin validate` — по той же причине, что
5
+ * и разбор манифеста.
6
+ *
7
+ * @module @reformer/builder-plugin-api/plugin/messages-bundle
8
+ */
9
+ /**
10
+ * Разбирает файл словаря: плоский объект «ключ → строка», и ничего больше.
11
+ *
12
+ * Вложенные объекты не разворачиваются сознательно: ключ у нас и так составной
13
+ * (`command.format`), и второй способ записать тот же ключ дал бы словарь, в котором промах
14
+ * ищется в двух местах. Отказ возвращается ПРИЧИНОЙ, а не готовой проблемой: файл и локаль
15
+ * знает только вызывающий, и собирать сообщение дважды незачем.
16
+ */
17
+ export declare function parseMessagesBundle(text: string): {
18
+ ok: true;
19
+ bundle: Readonly<Record<string, string>>;
20
+ } | {
21
+ ok: false;
22
+ reason: string;
23
+ cause?: unknown;
24
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Права плагина: что он объявляет в манифесте и что человек ему подтверждает.
3
+ *
4
+ * ## Почему поле появилось только сейчас
5
+ *
6
+ * «`permissions` не вводим» — решение, записанное в `plugin-and-shell.md` и действовавшее до
7
+ * этого модуля. Довод был не в том, что права не нужны, а в том, что **поле, которое ничего
8
+ * не принуждает, создаёт ложное ощущение границы**: объявленное намерение, которое никто
9
+ * не проверяет, хуже честного «включённый плагин может всё».
10
+ *
11
+ * Принуждение появилось вместе с первой привилегированной службой. Реестр служб отдаёт её
12
+ * только тому, кому право подтверждено, и отказ — не совет, а отсутствие объекта. Поэтому
13
+ * список здесь **закрытый и растёт ровно по одному имени на одну такую службу**: право,
14
+ * за которым не стоит запертой двери, — это снова то самое ложное ощущение.
15
+ *
16
+ * ## Что право НЕ значит
17
+ *
18
+ * Оно не про безопасность исполнения: плагин работает в том же realm, что оболочка, и код,
19
+ * который человек включил, может дотянуться до чего угодно своими руками. Граница здесь одна,
20
+ * и она названа в `plugin-and-shell.md`: плагин включается ЯВНО. Права сужают не возможности
21
+ * кода, а поверхность платформы — то, что оболочка подаёт ему сама, по адресу из контракта.
22
+ *
23
+ * @module @reformer/builder-plugin-api/plugin/permissions
24
+ */
25
+ /**
26
+ * Все права, которые оболочка умеет принуждать.
27
+ *
28
+ * - **`workspace.save`** — вынести написанное НАРУЖУ, в источник проекта. Единственная
29
+ * операция с таким свойством: все остальные пути записи упираются в рабочую копию, и до
30
+ * источника мимо сохранения не дотянуться физически. Отсюда и первое право: дверь ровно
31
+ * одна, охранять её дешевле, чем объяснять, почему её нет.
32
+ * - **`workspace.resources`** — менять записи проекта: создать, переименовать, перенести,
33
+ * удалить, скопировать, открыть другой каталог. То же свойство, что у сохранения: действие
34
+ * выходит за пределы рабочей копии, и отменить его нашими силами нельзя. Право заведено
35
+ * вместе со службой `reformer.workspace.resources`, а не «на будущее»: до неё эти операции
36
+ * существовали только портом композиции, то есть для внешнего плагина их не было вовсе,
37
+ * и запирать было нечего.
38
+ * - **`plugins.manage`** — распоряжаться ОСТАЛЬНЫМИ плагинами: включать, выключать,
39
+ * перезагружать, ставить из npm. Самое сильное из трёх, и это надо называть вслух:
40
+ * включить плагин значит исполнить чужой код, то есть обладатель права решает за человека
41
+ * то, что человек привык решать сам. Отдельным правом оно и заведено — чтобы вопрос
42
+ * задавался отдельно и заметно, а не приезжал довеском к работе с файлами.
43
+ */
44
+ export declare const PLUGIN_PERMISSIONS: readonly ["workspace.save", "workspace.resources", "plugins.manage"];
45
+ /** Право плагина — имя из {@link PLUGIN_PERMISSIONS}. */
46
+ export type PluginPermission = (typeof PLUGIN_PERMISSIONS)[number];
47
+ /** Знает ли оболочка такое право. Разбор манифеста отвергает незнакомые, а не игнорирует. */
48
+ export declare function isPluginPermission(value: string): value is PluginPermission;
@@ -0,0 +1,10 @@
1
+ import { Plugin } from './types.js';
2
+ /**
3
+ * Достаёт плагин из экспортов точки входа.
4
+ *
5
+ * Две формы, потому что их две в жизни: `module.exports = definePlugin(...)` у собранного
6
+ * `main.js` и `export default definePlugin(...)` у `main.ts`, который транспилируется
7
+ * в `exports.default`. Требовать одну из них значило бы отвергать половину рабочих плагинов
8
+ * ради формальности.
9
+ */
10
+ export declare function pluginFromExports(exports: unknown): Plugin | undefined;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Спецификаторы, которые оболочка подставляет исполняемому коду плагина СВОИМИ объектами.
3
+ *
4
+ * Это обещание рантайма, а не пожелание: сборка плагина обязана держать каждый из них ВНЕШНИМ.
5
+ * Вложи сборщик в `main.js` свою копию React или ядра форм — плагин получит второй экземпляр,
6
+ * и поломка будет тихой: два дерева хуков, `instanceof Signal`, всегда отвечающий `false`,
7
+ * вклады в чужой пустой реестр.
8
+ *
9
+ * Список — данные пакета, а не константа сборщика, по той же причине, что разбор манифеста:
10
+ * читают его двое. Оболочка регистрирует ровно эти модули (и тестом сверяет свой реестр
11
+ * с этим списком), инструменты автора плагина — выносят их из сборки. Разойдись два списка,
12
+ * и плагин, собранный «правильно», падал бы на спецификаторе, которого нет в реестре.
13
+ *
14
+ * Подпути перечислены поимённо: реестр резолвит ТОЧНЫМ совпадением, и `@reformer/cdk` не
15
+ * покрывает `@reformer/cdk/form-array`. Сборщику при этом годится и префикс — всё, что начинается
16
+ * с `@reformer/`, защищено оболочкой целиком и подменить его плагин не может.
17
+ *
18
+ * @module @reformer/builder-plugin-api/plugin/runtime-modules
19
+ */
20
+ export declare const PLUGIN_RUNTIME_MODULES: readonly string[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Изолированное хранилище плагина и его секреты.
3
+ *
4
+ * Два разных API, потому что у них разные умолчания по времени жизни: обычные данные плагин
5
+ * кладёт, чтобы они пережили перезагрузку страницы, а секрет — чтобы он **не** пережил её,
6
+ * пока не сказано обратное.
7
+ *
8
+ * ## `localStorage` не используется нигде
9
+ *
10
+ * В v1 в `localStorage` лежит ключ AI-провайдера — это ровно та ошибка, которую здесь
11
+ * не повторяем: `localStorage` синхронный (то есть блокирует поток на каждом обращении),
12
+ * общий на весь источник (то есть без пространств имён) и доступен любому скрипту на странице
13
+ * без единого рубежа. Постоянное хранилище — IndexedDB, и оно приходит сюда бэкендом.
14
+ *
15
+ * ## Почему бэкенд внедряется, а не импортируется
16
+ *
17
+ * Слой IndexedDB — соседняя работа (`host/workspace/storage/`). Если бы этот модуль импортировал
18
+ * его напрямую, изолированное хранилище нельзя было бы проверить без базы, а тест пространств
19
+ * имён превратился бы в тест IDB. Поэтому здесь объявлен минимальный контракт
20
+ * {@link PluginStorageBackend}, зеркалящий поверхность хранилища плюс пространство имён,
21
+ * и есть его памятная реализация {@link createMemoryStorageBackend}. Настоящий бэкенд подключает
22
+ * композиция, ничего в этом файле не меняя.
23
+ *
24
+ * ## Пространство имён — идентификатор плагина
25
+ *
26
+ * Честная оговорка из контракта: пока весь код на странице наш, изоляция пространств — гигиена,
27
+ * а не граница безопасности. Дотянуться до чужого пространства можно, и рантайм этому помешать
28
+ * не в состоянии. Ценность в другом: секрет не протечёт в будущий плагин по недосмотру, а XSS
29
+ * в одном месте не достанет ключ из другого. Подделать пространство плагин при этом не может:
30
+ * `pluginId` связывается в момент создания хранилища рантаймом — тем же приёмом, что
31
+ * `ExtensionRegistry.forPlugin`.
32
+ *
33
+ * Здесь ОБЪЯВЛЕНИЕ обоих хранилищ: их получает плагин полями контекста. Бэкенд и сборка
34
+ * (`createPluginStorage`, `createSecretStorage`) живут в оболочке билдера — она же связывает
35
+ * пространство имён с идентификатором плагина, и подделать его плагину нечем.
36
+ *
37
+ * @module @reformer/builder-plugin-api/plugin/storage
38
+ */
39
+ /** Данные плагина. Переживают перезагрузку страницы — если бэкенд постоянный. */
40
+ export interface PluginStorage {
41
+ /**
42
+ * Значение по ключу или `undefined`.
43
+ *
44
+ * `T` — обещание вызывающего, а не проверка: бэкенд хранит то, что положили, и после
45
+ * перезагрузки страницы там может лежать значение прежней версии плагина. Прочитанное
46
+ * проверяет тот, кто читает.
47
+ */
48
+ get<T>(key: string): Promise<T | undefined>;
49
+ /** Значение обязано быть структурно клонируемым: бэкенд — IndexedDB, а не JSON. */
50
+ set<T>(key: string, value: T): Promise<void>;
51
+ delete(key: string): Promise<void>;
52
+ /** Только ключи своего пространства имён. */
53
+ keys(): Promise<readonly string[]>;
54
+ }
55
+ /**
56
+ * Секреты плагина: токены, ключи провайдеров.
57
+ *
58
+ * Умолчание — память сессии. Это решение, а не недоделка: секрет, который переживает
59
+ * перезагрузку, обязан быть положен туда сознательным `persist`, потому что цена ошибки
60
+ * несимметрична — забытый в памяти секрет стоит одного повторного ввода, забытый на диске
61
+ * живёт до тех пор, пока о нём не вспомнят.
62
+ */
63
+ export interface SecretStorage {
64
+ get(key: string): Promise<string | undefined>;
65
+ /** По умолчанию — только память сессии. `persist` пишет в изолированное пространство. */
66
+ set(key: string, value: string, opts?: {
67
+ persist?: boolean;
68
+ }): Promise<void>;
69
+ delete(key: string): Promise<void>;
70
+ }