Перейти к содержимому

Структура пакета

Плагин — это обычный npm-пакет. Здесь описаны его части: манифест, правило зависимостей, шаблон плагина, артефакты сборки и их метаданные, формирование id плагина и состояние на диске. Манифест плагина — это файл package.json. Его верхний уровень парсится как .passthrough(), а блок bb и вложенный bb.branding — как .strict(), поэтому любой неизвестный ключ внутри bb ломает манифест.

ПолеОбяз.Что делаетОсобенность
nameдаnpm-идентификатор и единственный источник id плагинапо соглашению принято bb-plugin-<x>; пакеты со скоупом @acme/bb-plugin-<x> тоже работают; id всех плагинов из поставки bb зарезервированы
versionдазаписывается в артефактыуправляемая установка отклоняет dist/*.meta.json, если его pluginVersion не совпадает с манифестом
engines.bbнетsemver-диапазон для версии приложения; проверяется в рантайме и при выборе обновленияdev-сборки (0.0.0) не проверяются, но сообщают об этом в результате
engines.bbPluginSdkнетдиапазон SDK, читается как нижняя, а не верхняя границаотсутствие означает устаревший манифест. Установка из git: или npm: отклоняет плагин, если его SDK новее хоста или имеет другую мажорную версию; установка по локальному пути сообщает о статусе incompatible при загрузке
bb.nameдачеловекочитаемое имя в Settings, магазине и боковой панелиэто не name и не id плагина
bb.descriptionдаодно предложение: карточка в Browse и первый абзац страницыограничьтесь ~140 символами и синхронизируйте с PLUGIN_OVERVIEW.md
bb.brandingдаайдентика плагина; строгий объекттребуется хотя бы icon или logo.light
bb.branding.iconодно изимя иконки bb ("Zap") или путь к SVG в плагине ("./assets/icon.svg")здесь запрещён глиф с неймспейсом "<pluginId>/<name>"; путь в плагине должен заканчиваться на .svg
bb.branding.logoнет{ light, dark? }: более крупная айдентика для просторных мест.svg/.png/.webp; dark без light вызывает ошибку; bb plugin build отклоняет SVG-логотипы со скриптами в векторах; автоопределения логотипа в корне нет
bb.branding.experimental_iconsнетRecord<name, "./x.svg">: собственный словарь иконок плагина для строк таймлайна и значков провайдераимена соответствуют [a-z0-9-], начинаются с буквы, максимум 48 символов, максимум 64 записи; файлы до 32 КиБ; reject-only валидатор SVG (без script/style/iframe/foreignObject/image/a, on*, ссылок кроме #, xml:base); нарушение отменяет загрузку плагина и называет иконку
bb.serverдасерверная точка входаобязательна даже для плагинов только с UI: plugins/scheduled-send/server.ts — это намеренная заглушка. Установка по локальному пути загружает TypeScript напрямую, без сборки
bb.appнетклиентская точка входа → dist/app.js + app.css + app.meta.jsonустановки по локальному пути и из git собирают её при установке; npm-пакет должен поставлять готовый dist/
bb.hostнет, однаESM-точка входа Node 22 с полным доверием → dist/host.jsвызывается из собственной серверной точки входа через типизированный host RPC; демон скачивает её лениво и проверяет хеш-сумму
bb.skillsнетперемещает корни автоматически импортируемых скилловпо умолчанию skills/; [] отключает импорт. В репозитории значение всегда буквально ["skills"] (каталог, а не имена); 11 плагинов поставляют skills/ вообще без этого поля
bb.themesнетмассив { id, name, description?, css, codeTheme? } → Settings → Appearance и bb theme listid для выбора — plugin:<plugin-id>:<id>; вклад вносят только загруженные плагины. Ни один плагин из поставки bb не использует это поле

dependencies против devDependencies: асимметричное правило

Заголовок раздела «dependencies против devDependencies: асимметричное правило»

dependencies. Сюда идёт всё, что плагин импортирует в рантайме и что bb не заменяет шимами. Команда bb plugin build инлайнит эти зависимости, а установка из git скачивает только их. Если пакет нужен для сборки, но лежит в devDependencies, плагин не установится из git. Это касается и npm-запуска самой упакованной CLI: она работает с NODE_ENV=production.

Пакет zod намеренно оставили без шима. Слот для его неймспейса добавил бы 193 КБ (33 КБ в brotli) к загрузочному пейлоаду хоста. Поэтому zod бандлится в каждый плагин и живёт в dependencies.

Есть одно исключение. Плагин-провайдер держит @get-bb/plugin-sdk в dependencies, потому что хостовая сборка бандлит подпуть /provider-bridge из собственной установки плагина.

devDependencies. Сюда идут типы, инструменты сборки и любые пакеты, для которых есть шим. Единственный источник истины — RUNTIME_SLOT_BY_SPECIFIER. Если пакет с шимом окажется в dependencies, плагин притащит вторую копию синглтона вдобавок к хостовой.

Команда bb plugin types привязывает SDK и type-only зависимости в devDependencies к версиям запущенного bb. Флаг bb plugin types --check предназначен для CI: команда ничего не пишет на диск и завершается с ошибкой, если версии не совпадают.

// packages/plugin-build/src/runtime-shims.mjs — the full shim list
react · react-dom · react-dom/client · react/jsx-runtime · react/jsx-dev-runtime
@get-bb/plugin-sdk/app (+ legacy @bb/plugin-sdk/app)
@pierre/diffs · @pierre/diffs/react
@radix-ui/react-{alert-dialog, context-menu, dialog, dropdown-menu, hover-card,
menubar, navigation-menu, popover, select, tooltip}
sonner · vaul · clsx · tailwind-merge · class-variance-authority
@bb/shared-ui/icon · @bb/shared-ui/question-form-host

В этом же файле закреплены два критерия для шимов. Во-первых, семантика синглтонов: в приложении должен быть один React, одна вселенная порталов radix, вызов toast() должен попадать в тостер хоста, vaul мутирует document.body, а @pierre/diffs требует единого контекста. Во-вторых, общие библиотеки, которые иначе бандлились бы в каждый плагин: tailwind-merge ^3, clsx ^2, cva ^0.7 и компонент Icon из shared-ui (около 110 КБ на каждую копию карты hugeicons).

У этой команды нет флагов: ни --app, ни --headless, ни --yes. Шаблон плагина всегда включает фронтенд. Как сказано в README, для headless-плагина удалите bb.app, app.tsx, components/, hooks/ и lib/. Каталог всегда называется bb-plugin-<derived-id>, независимо от скоупа.

bb-plugin-<id>/
├── package.json # manifest: version 0.1.0, type module, engines {bb, bbPluginSdk}
├── server.ts # server entry: a todo store on bb.storage.kv → RPC + CLI + skill
├── app.tsx # app entry: definePluginApp + one navPanel
├── tsconfig.json # strict, ES2022, moduleResolution bundler, paths {"@/*": ["./*"]}
├── components.json # shadcn config; the @bb registry is pinned to the release tag of the current bb
├── .gitignore # exactly "dist/\nnode_modules/"
├── README.md # a long author's README
├── PLUGIN_OVERVIEW.md # store copy: ≤4000 chars (target 700–1800), ## sections, no HTML or images
├── skills/
│ └── example-todos/SKILL.md # agent instructions for the bb <id> command
├── components/ui/ # pre-vendored button, card, checkbox, dialog, input
│ ├── icon.tsx · icon-extended.tsx · icon-registry.ts
│ ├── motion.ts · overlay-trigger.ts · responsive-overlay.tsx · coarse-pointer-sizing.ts
│ └── hooks/ use-compact-viewport.tsx · use-media-query.ts · use-pointer-coarse.ts
├── hooks/useBrowserDimmingModal.ts
└── lib/ portal-scope.ts · utils.ts
АртефактФайлы
сервер (всегда)dist/server.js, server.js.map, server.meta.json
клиентская часть (если указан bb.app)dist/app.js, app.css, app.meta.json
хост (если указан bb.host)dist/host.js, host.js.map, host.meta.json

esbuild собирает все три артефакта во временную директорию внутри dist/ (через mkdtemp), а затем переименовывает их на месте.

packages/plugin-build/src/plugin-artifact-meta.ts
{
"sdkMajor": <PLUGIN_SDK_MAJOR>,
"sdkVersion": "<PLUGIN_SDK_VERSION>",
"artifactFormatVersion": 1,
"pluginId": "<derivePluginId(name)>",
"pluginVersion": "<package.json version>",
"builtWith": { "bbVersion": "…", "pluginSdkVersion": "…" }
}
// host.meta.json adds a seventh field:
// "artifactDigest": "<sha256 hex of host.js>"

На стороне потребителя validatePluginArtifactMeta проверяет метаданные: sdkMajor должен равняться текущему PLUGIN_SDK_MAJOR, а pluginId и pluginVersion — совпадать с манифестом. Поле builtWith.pluginSdkVersion должно быть валидным semver и совпадать с sdkVersion.

// packages/domain/src/plugin-id.ts — derivePluginId
1. if the package name is scoped, take the segment after "/"
2. strip the leading "bb-plugin-"
3. lowercase; any character outside [a-z0-9-] → "-"; trim "-" at the edges
4. an empty result throws
pluginIdSchema = /^[a-z0-9][a-z0-9-]*$/u
@acme/bb-plugin-Notes → notes
bb-plugin-simple-notes → simple-notes // directory plugins/docs, the only case of id ≠ directory

Этот id служит неймспейсом для маршрутов, хранилища, настроек и команд CLI. Кроме того, команда bb plugin new не даёт использовать зарезервированные имена системных команд.

<dataDir>/plugins/<id>/data.db its own SQLite (WAL, busy_timeout 5000)
<dataDir>/plugins/<id>/secrets/ secret settings, one file per key, + .http-token
<dataDir>/plugins/<id>/logs/plugin.log JSONL, rotated into plugin.log.1 at 5 MiB
<dataDir>/plugins/git/ · npm/ managed installs
<dataDir>/plugins/cache/git|npm/… materialized checkouts of the sources
<dataDir>/plugins/toolchain-* the downloaded esbuild/tailwind toolchain (cache it in CI)
<dataDir>/skills-generated/ the server-generated plugin-commands skill
<daemonDataDir>/plugins/<encoded id>/host-data the host side
<daemonDataDir>/plugin-host-artifacts/<encoded id>/<sha256>/host.mjs the artifact cache

По умолчанию dataDir указывает на ~/.bb/ для упакованного приложения. Запуск через pnpm dev перемещает его в ~/.bb-dev/<checkout-instance>/. Именно поэтому существует bb.server.experimental_dataDir — чтобы не приходилось угадывать путь к ~/.bb.