Расширяемые реестры (declare и extend)
Когда встроенных точек (core.page, core.nav, …) недостаточно, расширение может объявить собственный реестр — именованный словарь записей с вашим типом строки и стабильным registryId. Другие пакеты пополняют этот реестр по ключам (слотам).
Типичный сценарий: расширение «Чаты» объявляет реестр @rynt/chats-calls.message-types, а отдельные пакеты регистрируют тела сообщений для своих payload.type.
Две роли
| Роль | Манифест | Runtime | Пример |
|---|---|---|---|
| Автор реестра | declaresRegistries | defineExtensionRegistry<T>(id) + чтение через getExtensionRegistry(id) в UI | @rynt/chats-calls |
| Потребитель | extendsRegistries + extensionDependencies | getExtensionRegistry(id).register(key, value) в setup | @rynt/chats-message-sample |
Ключ слота (registryKey) — произвольная строка в рамках реестра. В чатах это payload.type сообщения (text, demo_ping, …).
→ Контракт манифеста: Маркетплейс / манифест
→ Модель хранения: Реестры: модель данных
Объявление реестра (автор)
Придумайте
registryId— уникальная строка, обычно<manifest.id>.<роль>(например@rynt/chats-calls.message-types).Опишите тип строки
T(поля объекта во втором аргументеregister).Экспортируйте константу и типы из npm-пакета (удобно для потребителей):
tsimport type { Component } from 'vue'; export const CHATS_MESSAGE_TYPES_REGISTRY_ID = '@rynt/chats-calls.message-types' as const; export interface ChatMessageTypeRegistration { body: Component; } declare module '@rynt/sdk/extension-registry-payload-map' { interface ExtensionRegistryPayloadMap { '@rynt/chats-calls.message-types': ChatMessageTypeRegistration; } }Заявите реестр в манифесте —
contributes.rynt.declaresRegistries:tsdeclaresRegistries: [ { id: '@rynt/chats-calls.message-types', title: 'Типы сообщений чата', description: 'Тела сообщений по ключу payload.type.', }, ],Зарегистрируйте встроенные ключи в
setup(под scope вашего расширения):tsimport { markRaw } from 'vue'; import { getExtensionRegistry } from '@rynt/sdk/extension'; getExtensionRegistry<ChatMessageTypeRegistration>( CHATS_MESSAGE_TYPES_REGISTRY_ID, ).register('text', { body: markRaw(TextMessageBody) }, 10);Третий аргумент
orderзадаёт порядок при переборе (не обязателен для точечногоget(key)).
Пополнение реестра (потребитель)
Зависимость в
package.jsonна npm-пакет автора (для типов и константregistryId).В манифесте —
extensionDependenciesиextendsRegistriesс явным спискомkeys:tsextensionDependencies: { '@rynt/chats-calls': '>=0.0.1', }, contributes: { rynt: { extendsRegistries: [ { registryId: '@rynt/chats-calls.message-types', keys: ['demo_ping'], }, ], }, },В
setup— регистрация записи:tsimport { markRaw } from 'vue'; import { defineRyntExtension } from '@rynt/sdk/extension'; import { getExtensionRegistry } from '@rynt/sdk/extension'; export default defineRyntExtension(() => { getExtensionRegistry('@rynt/chats-calls.message-types').register( 'demo_ping', { body: markRaw(DemoPingMessageBody) }, 80, ); });
→ Типизация контрактов — module augmentation для ExtensionRegistryPayloadMap
Проверка слота: есть ли обработчик ключа
Когда UI встречает ключ, для которого может не быть зарегистрированного расширения (сообщение неизвестного type, кнопка без провайдера), используйте утилиты из @rynt/sdk/extension:
| Функция | Назначение |
|---|---|
hasRegistryEntry(registryId, key) | Быстрая проверка «есть запись в local store» |
getRegistryEntryRow(registryId, key) | Строка реестра или undefined: value, extensionId автора записи, order |
resolveRegistrySlot(registryId, key) | Дискриминированный результат: { status: 'present', row } или { status: 'missing' } |
Пример в computed (как в Message.vue расширения чатов):
import { computed } from 'vue';
import {
extensionRegistriesRevision,
getExtensionRegistry,
} from '@rynt/sdk/extension';
const msgType = computed(() => props.payload?.type || 'text');
const registryBody = computed(() => {
// Реактивность: после reload / install расширения слот может появиться
void extensionRegistriesRevision.value;
return getExtensionRegistry<ChatMessageTypeRegistration>(
CHATS_MESSAGE_TYPES_REGISTRY_ID,
).get(msgType.value)?.body;
});extensionRegistriesRevision — счётчик любых изменений реестров; читайте его в computed / watch, чтобы UI обновился после установки или dev-reload расширения.
Для ветвления без доступа к value:
import { resolveRegistrySlot } from '@rynt/sdk/extension';
const slot = resolveRegistrySlot(
CHATS_MESSAGE_TYPES_REGISTRY_ID,
msgType.value,
);
if (slot.status === 'present') {
const { body } = slot.row.value;
// …
}Кнопка «Установить расширение для слота»
ExtensionRegistrySlotButton (@rynt/sdk) — готовый UI-компонент для случая resolveRegistrySlot → missing. Показывает предложение установить расширение из маркетплейса по паре (registryId, registryKey).
Props
| Prop | Тип | Описание |
|---|---|---|
registryId | string | Id реестра (тот же, что в declaresRegistries) |
registryKey | string | Ключ слота (не путать с Vue key) |
label? | string | Подпись, если в каталоге несколько кандидатов |
onResolved? | () => void | Callback после успешной установки и reload |
Поведение
- Локально:
resolveRegistrySlot— еслиpresent, компонент не отображается (обработчик уже есть). - Если
missing— асинхронныйlookupSlot({ registryId, key })черезinject(RYNT_EXTENSION_MARKETPLACE_SLOT_RESOLVE)(provide задаёт лаунчер). one— кнопка с именем расширения и прямой установкой;many/ fallback — открытие диалога маркетплейсаmode: 'slot'.
SDK не импортирует диалог лаунчера: хост в корне приложения делает provide для RYNT_EXTENSION_MARKETPLACE_OPEN, RYNT_EXTENSION_MARKETPLACE_SLOT_RESOLVE, RYNT_EXTENSION_MARKETPLACE_INSTALL.
→ Подробнее: UI лаунчера и маркетплейс
Импорт
import { ExtensionRegistrySlotButton } from '@rynt/sdk';Пример: типы сообщений в чатах
Полный справочник по чатам (реестры, expose, манифест, встроенные типы): Чаты: реестры и интеграция.
Полный цикл в репозитории: @rynt/chats-calls + @rynt/chats-message-sample.
Рендер в Message.vue
<!-- Тело из реестра -->
<component
v-else-if="registryBody"
:is="registryBody"
:payload="payload"
…
/>
<!-- Fallback: тип не зарегистрирован -->
<div v-else class="space-y-2">
<div class="text-subtle-fg italic text-sm">
Не поддерживаемый тип «{{ msgType }}» сообщения.
</div>
<ExtensionRegistrySlotButton
:registry-id="CHATS_MESSAGE_TYPES_REGISTRY_ID"
:registry-key="msgType"
/>
</div>Логика:
msgType=payload.type(по умолчаниюtext).registryBody— компонент из реестра, если ключ зарегистрирован (встроенно или сторонним расширением).- Иначе — текст +
ExtensionRegistrySlotButton: пользователь может установить пакет, который заявил этотkeyвextendsRegistries.
После установки лаунчер перезагружает расширения → extensionRegistriesRevision растёт → registryBody находит demo_ping → кнопка исчезает, рендерится DemoPingMessageBody.
Схема
Контракт для npm
Чтобы другим было проще:
- экспортируйте константу с
registryIdи типыT; - выполните
declare module '@rynt/sdk/extension-registry-payload-map'— тогдаgetExtensionRegistry(MY_ID)типизируется без дженерика; - задокументируйте семантику ключей (как
payload.type, id кнопки тулбара и т.д.).