188 lines
9.0 KiB
Markdown
188 lines
9.0 KiB
Markdown
# Milestone 5b — Frontend Bundle Host / Plugin API Stub
|
||
|
||
## Цель
|
||
|
||
Первый настоящий frontend plugin host слой: shell умеет загружать frontend bundle плагина, давать ему ограниченный VerstakPluginAPI stub и рендерить view/settings panel через зарегистрированный component id.
|
||
|
||
## Что реализовано
|
||
|
||
### 1. Bundle Contract
|
||
|
||
Плагин регистрирует компоненты через глобальную функцию:
|
||
|
||
```javascript
|
||
window.VerstakPluginRegister('verstak.platform-test', {
|
||
components: {
|
||
'DiagnosticsPanel': {
|
||
mount: function(containerEl, props, api) {
|
||
// Рендерит UI в containerEl
|
||
// containerEl — div, созданный PluginBundleHost
|
||
// api — ограниченный VerstakPluginAPI
|
||
},
|
||
unmount: function(containerEl) {
|
||
// Очистка при смене view/unmount
|
||
containerEl.innerHTML = '';
|
||
}
|
||
},
|
||
'PlatformTestSettings': {
|
||
mount: function(containerEl, props, api) { /*...*/ },
|
||
unmount: function(containerEl) { /*...*/ }
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
**VerstakPluginAPI** — ограниченный API, передаваемый в mount():
|
||
|
||
| Метод | Статус | Описание |
|
||
|---|---|---|
|
||
| `api.pluginId` | ✅ Работает | ID плагина |
|
||
| `api.capabilities.has(id)` | ✅ Stub | Возвращает false (planned: реальный запрос к registry) |
|
||
| `api.events.publish(type, payload)` | ✅ Stub | Логирует в console (planned: event bus bridge) |
|
||
| `api.events.subscribe(type, handler)` | ✅ Stub | Логирует в console (planned: event bus bridge) |
|
||
| `api.settings.read(key)` | ✅ Stub | Возвращает null (planned: backend storage namespace) |
|
||
| `api.settings.write(key, value)` | ✅ Stub | Логирует в console (planned: backend storage) |
|
||
| `api.commands.execute(id, args)` | ✅ Stub | Логирует в console (planned: command execution) |
|
||
|
||
### 2. Безопасная резолюция asset path
|
||
|
||
**Backend методы:**
|
||
|
||
| Метод | Описание |
|
||
|---|---|
|
||
| `GetPluginFrontendInfo(pluginID)` | Возвращает frontend metadata (entry, style, rootPath, name, icon, version) |
|
||
| `GetPluginAssetContent(pluginID, assetPath)` | Читает файл из директории плагина с валидацией безопасности |
|
||
|
||
**Проверки безопасности:**
|
||
- Абсолютные пути (начинающиеся с `/` или `\`) — отклоняются
|
||
- Path traversal (`..`) — отклоняется
|
||
- Выход за пределы plugin root — отклоняется через `filepath.Abs` + `strings.HasPrefix`
|
||
|
||
### 3. FrontendPluginHost
|
||
|
||
**PluginBundleHost.svelte** — загружает и рендерит плагин бандлы:
|
||
|
||
1. Получает plugin frontend info через `GetPluginFrontendInfo()`
|
||
2. Если у плагина есть frontend entry — загружает JS контент через `GetPluginAssetContent()`
|
||
3. Выполняет bundle через `new Function(content)` (безопасно: нет доступа к внешней области видимости)
|
||
4. Ждёт вызов `VerstakPluginRegister` и находит компонент по componentId
|
||
5. Создаёт `VerstakPluginAPI` и вызывает `component.mount(container, props, api)`
|
||
6. При смене view — вызывает `component.unmount(container)` и очищает
|
||
|
||
**Error boundary:**
|
||
- Если bundle не загружается — fallback с pluginID, componentId, error text
|
||
- Если компонент не найден — показывает доступные components
|
||
- Если mount выбрасывает исключение — fallback без падения shell
|
||
- Все состояния: idle, loading, error, loaded
|
||
|
||
### 4. ViewContainer.svelte
|
||
|
||
- Проверяет наличие frontend bundle у плагина
|
||
- Если есть — рендерит PluginBundleHost
|
||
- Если нет — показывает "frontend bundle not available" placeholder
|
||
- Badge в заголовке: "frontend bundle" (зелёный) или "no frontend bundle" (красный)
|
||
|
||
### 5. PluginManager — Settings Panel
|
||
|
||
- Убран hardcoded platform-test settings form
|
||
- Settings panel рендерится через PluginBundleHost, если у плагина есть frontend entry
|
||
- Если нет — показывает "Settings panel frontend bundle not available"
|
||
|
||
### 6. platform-test plugin — Real Frontend Bundle
|
||
|
||
**`frontend/dist/index.js`** (14.6 KB):
|
||
- Регистрирует компоненты через `VerstakPluginRegister`
|
||
- Диагностическая панель:
|
||
- Plugin name, version, ID
|
||
- "✅ Frontend Bundle Loaded" badge
|
||
- Test results summary
|
||
- Capabilities status section
|
||
- API methods info
|
||
- Settings panel:
|
||
- Plugin name + ID
|
||
- Interactive counter (increment/decrement/reset)
|
||
- Demo settings list
|
||
- Темная тема (совпадает с shell)
|
||
|
||
**`frontend/style.css`** (4.9 KB):
|
||
- Shared dark-theme styles
|
||
- Используется обоими компонентами
|
||
|
||
### 7. Тесты
|
||
|
||
**Backend (11 новых тестов в `internal/api/app_test.go`):**
|
||
| Тест | Проверяет |
|
||
|---|---|
|
||
| GetPluginFrontendInfo (known) | Полные данные для плагина с frontend |
|
||
| GetPluginFrontendInfo (no frontend) | Статус "no-frontend" |
|
||
| GetPluginFrontendInfo (unknown) | Статус "not-found" |
|
||
| GetPluginAssetContent (existing) | Чтение существующего файла |
|
||
| GetPluginAssetContent (style) | Чтение style.css |
|
||
| GetPluginAssetContent (absolute path) | Отклонение `/` и `\` |
|
||
| GetPluginAssetContent (path traversal) | Отклонение `..` |
|
||
| GetPluginAssetContent (path escape) | Отклонение выхода за root |
|
||
| GetPluginAssetContent (not found) | Ошибка для неизвестного pluginID |
|
||
| GetPluginAssetContent (no frontend) | Ошибка если нет frontend |
|
||
| GetPluginAssetContent (missing file) | Ошибка если файл не существует |
|
||
|
||
**Smoke test (frontend bundle checks):**
|
||
- Manifest объявляет `frontend.entry = "frontend/dist/index.js"`
|
||
- Файл бандла существует на диске
|
||
- Бандл содержит `"VerstakPluginRegister"`
|
||
- Компоненты `DiagnosticsPanel` и `PlatformTestSettings` зарегистрированы
|
||
|
||
### 8. Security Constraints
|
||
|
||
| Сценарий | Результат |
|
||
|---|---|
|
||
| frontend entry `../etc/passwd` | Отклоняется (path traversal) |
|
||
| frontend entry `/etc/passwd` | Отклоняется (absolute path) |
|
||
| Плагин без frontend | Не ломает UI, показывает placeholder |
|
||
| Плагин с missing entry | Error fallback с понятным сообщением |
|
||
| Bundle execution error | Error fallback, shell не падает |
|
||
| Компонент не найден в bundle | Error fallback со списком доступных components |
|
||
|
||
## Изменённые файлы
|
||
|
||
### verstak-desktop
|
||
|
||
| Файл | Изменение |
|
||
|---|---|
|
||
| `internal/api/app.go` | + `GetPluginFrontendInfo()`, `GetPluginAssetContent()` с path validation |
|
||
| `internal/api/app_test.go` | **NEW**: 11 тестов |
|
||
| `frontend/src/lib/plugin-host/VerstakPluginAPI.js` | **NEW**: Bundle contract + API stub |
|
||
| `frontend/src/lib/plugin-host/PluginBundleHost.svelte` | **NEW**: Загрузка/рендер бандлов, error boundary |
|
||
| `frontend/src/lib/shell/ViewContainer.svelte` | Обновлён: PluginBundleHost вместо placeholder |
|
||
| `frontend/src/lib/plugin-manager/PluginManager.svelte` | Обновлён: Settings через PluginBundleHost |
|
||
| `cmd/smoke-platform/main.go` | Обновлён: frontend bundle checks |
|
||
| `docs/PLUGIN_RUNTIME.md` | Обновлён: bundle contract, security |
|
||
|
||
### verstak-official-plugins
|
||
|
||
| Файл | Изменение |
|
||
|---|---|
|
||
| `plugins/platform-test/frontend/src/index.js` | **NEW**: DiagnosticsPanel + SettingsPanel |
|
||
| `plugins/platform-test/frontend/style.css` | **NEW**: Dark theme styles |
|
||
| `plugins/platform-test/frontend/dist/index.js` | **REPLACED**: VerstakPluginRegister contract |
|
||
|
||
### verstak-docs
|
||
| `docs/MILESTONE_PLATFORM_RUNTIME_5b.md` | **NEW**: этот документ |
|
||
|
||
## Проверки
|
||
|
||
```
|
||
go test ./internal/... -count=1 → ✅ 56 PASS (all packages)
|
||
go vet ./... → ✅ clean
|
||
cd frontend && npm run build → ✅ built (72.98 KB gzip:21.64 KB)
|
||
bash scripts/smoke-platform.sh → ✅ 4 теста (plugin + enable/disable + workspace + contributions + frontend bundle)
|
||
bash scripts/build.sh → ✅ wails build
|
||
```
|
||
|
||
## Non-goals (не реализовано)
|
||
- Official notes/files/editor extraction
|
||
- Backend sidecar runtime
|
||
- Secrets
|
||
- Полноценный command palette
|
||
- Remote plugin registry
|
||
- Прямой доступ плагина к Wails backend methods (кроме VerstakPluginAPI)
|