Skip to content

Расширяемые реестры (declare и extend)

Когда встроенных точек (core.page, core.nav, …) недостаточно, расширение может объявить собственный реестр — именованный словарь записей с вашим типом строки и стабильным registryId. Другие пакеты пополняют этот реестр по ключам (слотам).

Типичный сценарий: расширение «Чаты» объявляет реестр @rynt/chats-calls.message-types, а отдельные пакеты регистрируют тела сообщений для своих payload.type.

Две роли

РольМанифестRuntimeПример
Автор реестраdeclaresRegistriesdefineExtensionRegistry<T>(id) + чтение через getExtensionRegistry(id) в UI@rynt/chats-calls
ПотребительextendsRegistries + extensionDependenciesgetExtensionRegistry(id).register(key, value) в setup@rynt/chats-message-sample

Ключ слота (registryKey) — произвольная строка в рамках реестра. В чатах это payload.type сообщения (text, demo_ping, …).

→ Контракт манифеста: Маркетплейс / манифест
→ Модель хранения: Реестры: модель данных


Объявление реестра (автор)

  1. Придумайте registryId — уникальная строка, обычно <manifest.id>.<роль> (например @rynt/chats-calls.message-types).

  2. Опишите тип строки T (поля объекта во втором аргументе register).

  3. Экспортируйте константу и типы из npm-пакета (удобно для потребителей):

    ts
    import 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;
      }
    }
  4. Заявите реестр в манифестеcontributes.rynt.declaresRegistries:

    ts
    declaresRegistries: [
      {
        id: '@rynt/chats-calls.message-types',
        title: 'Типы сообщений чата',
        description: 'Тела сообщений по ключу payload.type.',
      },
    ],
  5. Зарегистрируйте встроенные ключи в setup (под scope вашего расширения):

    ts
    import { 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)).


Пополнение реестра (потребитель)

  1. Зависимость в package.json на npm-пакет автора (для типов и констант registryId).

  2. В манифесте — extensionDependencies и extendsRegistries с явным списком keys:

    ts
    extensionDependencies: {
      '@rynt/chats-calls': '>=0.0.1',
    },
    contributes: {
      rynt: {
        extendsRegistries: [
          {
            registryId: '@rynt/chats-calls.message-types',
            keys: ['demo_ping'],
          },
        ],
      },
    },
  3. В setup — регистрация записи:

    ts
    import { 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 расширения чатов):

ts
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:

ts
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ТипОписание
registryIdstringId реестра (тот же, что в declaresRegistries)
registryKeystringКлюч слота (не путать с Vue key)
label?stringПодпись, если в каталоге несколько кандидатов
onResolved?() => voidCallback после успешной установки и reload

Поведение

  1. Локально: resolveRegistrySlot — если present, компонент не отображается (обработчик уже есть).
  2. Если missing — асинхронный lookupSlot({ registryId, key }) через inject(RYNT_EXTENSION_MARKETPLACE_SLOT_RESOLVE) (provide задаёт лаунчер).
  3. one — кнопка с именем расширения и прямой установкой; many / fallback — открытие диалога маркетплейса mode: 'slot'.

SDK не импортирует диалог лаунчера: хост в корне приложения делает provide для RYNT_EXTENSION_MARKETPLACE_OPEN, RYNT_EXTENSION_MARKETPLACE_SLOT_RESOLVE, RYNT_EXTENSION_MARKETPLACE_INSTALL.

→ Подробнее: UI лаунчера и маркетплейс

Импорт

ts
import { ExtensionRegistrySlotButton } from '@rynt/sdk';

Пример: типы сообщений в чатах

Полный справочник по чатам (реестры, expose, манифест, встроенные типы): Чаты: реестры и интеграция.

Полный цикл в репозитории: @rynt/chats-calls + @rynt/chats-message-sample.

Рендер в Message.vue

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>

Логика:

  1. msgType = payload.type (по умолчанию text).
  2. registryBody — компонент из реестра, если ключ зарегистрирован (встроенно или сторонним расширением).
  3. Иначе — текст + 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 кнопки тулбара и т.д.).

См. также