Compare commits
No commits in common. "a858a928bf72cbd75664c5370a40e83a84c89d24" and "ad2dd975e7f88746c8a677220e6500ca72287b20" have entirely different histories.
a858a928bf
...
ad2dd975e7
|
|
@ -1,3 +1 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
|
||||||
release/
|
|
||||||
|
|
|
||||||
17
LICENSE
17
LICENSE
|
|
@ -1,17 +0,0 @@
|
||||||
Verstak Plugin SDK
|
|
||||||
Copyright (C) 2026 Verstak contributors
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under
|
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
|
||||||
Software Foundation, either version 3 of the License, or (at your option) any
|
|
||||||
later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
details.
|
|
||||||
|
|
||||||
The complete license text is available at:
|
|
||||||
https://www.gnu.org/licenses/agpl-3.0.html
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
41
README.md
41
README.md
|
|
@ -1,38 +1,6 @@
|
||||||
# Verstak Plugin SDK
|
# verstak-sdk
|
||||||
|
|
||||||
TypeScript API, JSON schemas and contract tests for plugins running in Verstak
|
Verstak Plugin SDK — manifest schema, TypeScript SDK, RPC protocol, capability contracts, event schemas, test helpers, packaging tools
|
||||||
Desktop. The SDK is versioned independently so plugin authors can validate
|
|
||||||
their manifests and compile against the public host API.
|
|
||||||
|
|
||||||
> **Alpha contract.** This is the first public alpha. Keep SDK, Desktop and
|
|
||||||
> official-plugin versions in the same release line while APIs are evolving.
|
|
||||||
|
|
||||||
## Install and verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm ci
|
|
||||||
npm run lint
|
|
||||||
npm test
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
The build emits `dist/`. A packed npm artifact can be made locally with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/release.sh v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
It validates the requested version against `package.json`, then writes an npm
|
|
||||||
tarball and `SHA256SUMS` to `release/`.
|
|
||||||
|
|
||||||
## Contracts relevant to the alpha
|
|
||||||
|
|
||||||
- Workspaces have durable UUID identities; paths are addresses, not identity.
|
|
||||||
- Activity may be scoped to `workspaceId` or to explicit `unassigned` work.
|
|
||||||
- The `hostname-normalization-v1.json` vectors define the shared canonical
|
|
||||||
browser-domain representation used by Desktop and the extension.
|
|
||||||
- Browser activity batches contain only a normalized hostname and bounded
|
|
||||||
duration. Manual captures use a separate Inbox protocol.
|
|
||||||
|
|
||||||
## Bundled Frontend API Contract
|
## Bundled Frontend API Contract
|
||||||
|
|
||||||
|
|
@ -72,8 +40,3 @@ or trash metadata.
|
||||||
Bundled frontend plugins are trusted/cooperative and run in the desktop JS
|
Bundled frontend plugins are trusted/cooperative and run in the desktop JS
|
||||||
context. Current permission checks are contract checks, not a security boundary;
|
context. Current permission checks are contract checks, not a security boundary;
|
||||||
real isolation belongs to a later sidecar/sandbox milestone.
|
real isolation belongs to a later sidecar/sandbox milestone.
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright © 2026 Verstak contributors. Licensed under
|
|
||||||
[GNU AGPLv3 or later](LICENSE).
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import type { CapabilityEntry, FileBytes, FileEntry, FileMetadata, MovePathOptions, OpenResourceRequest, OpenResourceResult, PluginSettings, RegisteredContributionPoints, RestoreTrashOptions, TrashEntry, TrashResult, WriteTextOptions } from './types';
|
import type { CapabilityEntry, FileBytes, FileEntry, FileMetadata, MovePathOptions, OpenResourceRequest, OpenResourceResult, PluginSettings, RegisteredContributionPoints, RestoreTrashOptions, TrashEntry, TrashResult, WriteTextOptions } from './types';
|
||||||
export type PluginCommandArgs = Record<string, unknown>;
|
export type PluginCommandArgs = Record<string, unknown>;
|
||||||
export type PluginDataJSON = Record<string, unknown>;
|
export type PluginDataJSON = Record<string, unknown>;
|
||||||
export type PluginLocale = 'ru' | 'en';
|
|
||||||
export type TranslationParams = Record<string, string | number>;
|
|
||||||
export type PluginCommandHandler = (args: PluginCommandArgs, declaration: PluginCommandDeclaration) => unknown | Promise<unknown>;
|
export type PluginCommandHandler = (args: PluginCommandArgs, declaration: PluginCommandDeclaration) => unknown | Promise<unknown>;
|
||||||
export type Unsubscribe = () => void;
|
export type Unsubscribe = () => void;
|
||||||
export interface PluginCommandDeclaration {
|
export interface PluginCommandDeclaration {
|
||||||
|
|
@ -63,11 +61,6 @@ export interface BrowserReceiverPairing {
|
||||||
}
|
}
|
||||||
export interface VerstakPluginAPI {
|
export interface VerstakPluginAPI {
|
||||||
readonly pluginId: string;
|
readonly pluginId: string;
|
||||||
i18n: {
|
|
||||||
getLocale(): PluginLocale;
|
|
||||||
t(key: string, params?: TranslationParams, fallback?: string): string;
|
|
||||||
onDidChangeLocale(listener: (locale: PluginLocale) => void): Unsubscribe;
|
|
||||||
};
|
|
||||||
settings: {
|
settings: {
|
||||||
read(): Promise<PluginSettings>;
|
read(): Promise<PluginSettings>;
|
||||||
read<T = unknown>(key: string): Promise<T | undefined>;
|
read<T = unknown>(key: string): Promise<T | undefined>;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AA8LhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
|
{"version":3,"file":"plugin-api.js","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,8EAA8E;AAC9E,gDAAgD;AAsLhD,MAAM,UAAU,eAAe,CAAC,SAAiB;IAC/C,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;AACtF,CAAC"}
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
||||||
import type { PluginLocale, VerstakPluginAPI } from './plugin-api';
|
import type { VerstakPluginAPI } from './plugin-api';
|
||||||
export interface MockPluginAPIOptions {
|
export interface MockPluginAPIOptions {
|
||||||
contributions?: RegisteredContributionPoints;
|
contributions?: RegisteredContributionPoints;
|
||||||
locale?: PluginLocale;
|
|
||||||
defaultLocale?: PluginLocale;
|
|
||||||
messages?: Partial<Record<PluginLocale, Record<string, string>>>;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Создать тестовый manifest для unit-тестов.
|
* Создать тестовый manifest для unit-тестов.
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,YAAY,EAAqB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ5G,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;IAC7C,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClE;AASD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,CA0XlH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
|
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,KAAK,EAAwB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQ3E,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,4BAA4B,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAetF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CASnF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAgB,EAAE,OAAO,GAAE,oBAAyB,GAAG,gBAAgB,CA4WlH;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgCxF;AAGD,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,CAAC"}
|
||||||
|
|
@ -3,11 +3,6 @@ const mockCommandHandlers = new Map();
|
||||||
function commandKey(pluginId, commandId) {
|
function commandKey(pluginId, commandId) {
|
||||||
return `${pluginId}:${commandId}`;
|
return `${pluginId}:${commandId}`;
|
||||||
}
|
}
|
||||||
function interpolateMessage(message, params) {
|
|
||||||
if (!params)
|
|
||||||
return message;
|
|
||||||
return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, (placeholder, name) => (Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : placeholder));
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Создать тестовый manifest для unit-тестов.
|
* Создать тестовый manifest для unit-тестов.
|
||||||
*/
|
*/
|
||||||
|
|
@ -44,9 +39,6 @@ export function createTestPluginState(overrides) {
|
||||||
* Создать заглушку VerstakPluginAPI для тестов.
|
* Создать заглушку VerstakPluginAPI для тестов.
|
||||||
*/
|
*/
|
||||||
export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||||
const locale = options.locale || 'en';
|
|
||||||
const defaultLocale = options.defaultLocale || 'en';
|
|
||||||
const messages = options.messages || {};
|
|
||||||
const settings = {};
|
const settings = {};
|
||||||
const pluginData = new Map();
|
const pluginData = new Map();
|
||||||
const commands = new Map();
|
const commands = new Map();
|
||||||
|
|
@ -137,17 +129,6 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options = {}) {
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pluginId,
|
pluginId,
|
||||||
i18n: {
|
|
||||||
getLocale: vi.fn(() => locale),
|
|
||||||
t: vi.fn((key, params, fallback) => {
|
|
||||||
const message = messages[locale]?.[key]
|
|
||||||
?? messages[defaultLocale]?.[key]
|
|
||||||
?? fallback
|
|
||||||
?? key;
|
|
||||||
return interpolateMessage(message, params);
|
|
||||||
}),
|
|
||||||
onDidChangeLocale: vi.fn((_listener) => () => { }),
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
read: vi.fn(async (key) => key ? settings[key] : { ...settings }),
|
read: vi.fn(async (key) => key ? settings[key] : { ...settings }),
|
||||||
write: vi.fn(async (key, value) => {
|
write: vi.fn(async (key, value) => {
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +1,4 @@
|
||||||
export type PluginSource = 'official' | 'local' | 'third-party';
|
export type PluginSource = 'official' | 'local' | 'third-party';
|
||||||
export interface PluginLocalizationConfig {
|
|
||||||
defaultLocale: string;
|
|
||||||
locales: Record<string, string>;
|
|
||||||
}
|
|
||||||
export interface PluginManifest {
|
export interface PluginManifest {
|
||||||
schemaVersion: 1;
|
schemaVersion: 1;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -12,7 +8,6 @@ export interface PluginManifest {
|
||||||
description?: string;
|
description?: string;
|
||||||
source?: PluginSource;
|
source?: PluginSource;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
localization?: PluginLocalizationConfig;
|
|
||||||
provides: string[];
|
provides: string[];
|
||||||
requires?: string[];
|
requires?: string[];
|
||||||
optionalRequires?: string[];
|
optionalRequires?: string[];
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -2,7 +2,6 @@
|
||||||
"name": "@verstak/plugin-sdk",
|
"name": "@verstak/plugin-sdk",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Verstak Plugin SDK — TypeScript types and runtime API for plugin development",
|
"description": "Verstak Plugin SDK — TypeScript types and runtime API for plugin development",
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
||||||
"$id": "https://git.mirv.top/verstak/verstak-sdk/schemas/hostname-normalization-v1.json",
|
|
||||||
"title": "Verstak canonical hostname normalization v1 test vectors",
|
|
||||||
"description": "Canonical hostnames are lowercase ASCII A-labels without a port or trailing DNS dot. Bare hostnames accept DNS names, IPv4, bracketed IPv6, localhost, and internal single-label names. URL inputs accept only HTTP(S). Invalid or excessively long input normalizes to an empty string.",
|
|
||||||
"version": 1,
|
|
||||||
"bare": [
|
|
||||||
{ "input": "example.com", "output": "example.com" },
|
|
||||||
{ "input": " Example.COM. ", "output": "example.com" },
|
|
||||||
{ "input": "пример.рф", "output": "xn--e1afmkfd.xn--p1ai" },
|
|
||||||
{ "input": "bücher.example", "output": "xn--bcher-kva.example" },
|
|
||||||
{ "input": "127.0.0.1", "output": "127.0.0.1" },
|
|
||||||
{ "input": "[2001:db8::1]", "output": "2001:db8::1" },
|
|
||||||
{ "input": "localhost", "output": "localhost" },
|
|
||||||
{ "input": "intranet", "output": "intranet" },
|
|
||||||
{ "input": "", "output": "" },
|
|
||||||
{ "input": "https://example.com", "output": "" },
|
|
||||||
{ "input": "example.com:443", "output": "" },
|
|
||||||
{ "input": "user@example.com", "output": "" },
|
|
||||||
{ "input": "bad host", "output": "" },
|
|
||||||
{ "input": "example..com", "output": "" },
|
|
||||||
{ "input": "example.com..", "output": "" },
|
|
||||||
{ "input": "127.000.000.001", "output": "" },
|
|
||||||
{ "input": "[2001:db8::1]:443", "output": "" },
|
|
||||||
{ "input": "a...............................................................example", "output": "" }
|
|
||||||
],
|
|
||||||
"url": [
|
|
||||||
{ "input": "https://пример.рф/path", "output": "xn--e1afmkfd.xn--p1ai" },
|
|
||||||
{ "input": "http://Example.COM.:8080/path", "output": "example.com" },
|
|
||||||
{ "input": "https://[2001:db8::1]/", "output": "2001:db8::1" },
|
|
||||||
{ "input": "ftp://example.com/path", "output": "" },
|
|
||||||
{ "input": "not a URL", "output": "" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -51,31 +51,6 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to plugin icon (relative to plugin root)"
|
"description": "Path to plugin icon (relative to plugin root)"
|
||||||
},
|
},
|
||||||
"localization": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Plugin-owned localization catalogs",
|
|
||||||
"properties": {
|
|
||||||
"defaultLocale": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Default locale used when the selected locale is unavailable",
|
|
||||||
"pattern": "^[a-z]{2}(?:-[a-z0-9]+)*$"
|
|
||||||
},
|
|
||||||
"locales": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Locale tags mapped to plugin-relative JSON catalog paths",
|
|
||||||
"propertyNames": {
|
|
||||||
"pattern": "^[a-z]{2}(?:-[a-z0-9]+)*$"
|
|
||||||
},
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^(?![\\\\/])(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"
|
|
||||||
},
|
|
||||||
"minProperties": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["defaultLocale", "locales"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
"provides": {
|
"provides": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Capabilities this plugin provides",
|
"description": "Capabilities this plugin provides",
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
REQUESTED_VERSION="${1:-}"
|
|
||||||
PACKAGE_VERSION="$(node -p "require('$ROOT/package.json').version")"
|
|
||||||
|
|
||||||
if [[ -z "$REQUESTED_VERSION" ]]; then
|
|
||||||
echo "usage: $0 <version>" >&2
|
|
||||||
echo "example: $0 v0.1.0" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ "${REQUESTED_VERSION#v}" != "$PACKAGE_VERSION" ]]; then
|
|
||||||
echo "requested version $REQUESTED_VERSION does not match package.json $PACKAGE_VERSION" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== verstak sdk release $PACKAGE_VERSION ==="
|
|
||||||
(cd "$ROOT" && npm ci --no-audit --no-fund && npm run lint && npm test && npm run build)
|
|
||||||
|
|
||||||
RELEASE_ROOT="$ROOT/release"
|
|
||||||
rm -rf "$RELEASE_ROOT"
|
|
||||||
mkdir -p "$RELEASE_ROOT"
|
|
||||||
(cd "$ROOT" && npm pack --pack-destination "$RELEASE_ROOT")
|
|
||||||
(cd "$RELEASE_ROOT" && sha256sum ./*.tgz > SHA256SUMS)
|
|
||||||
|
|
||||||
echo "release package: $RELEASE_ROOT"
|
|
||||||
echo "checksums: $RELEASE_ROOT/SHA256SUMS"
|
|
||||||
|
|
@ -16,9 +16,6 @@ describe('VerstakPluginAPI contract', () => {
|
||||||
expect(typeof api.settings.write).toBe('function');
|
expect(typeof api.settings.write).toBe('function');
|
||||||
expect(typeof api.storage.data.read).toBe('function');
|
expect(typeof api.storage.data.read).toBe('function');
|
||||||
expect(typeof api.storage.data.write).toBe('function');
|
expect(typeof api.storage.data.write).toBe('function');
|
||||||
expect(typeof api.i18n.getLocale).toBe('function');
|
|
||||||
expect(typeof api.i18n.t).toBe('function');
|
|
||||||
expect(typeof api.i18n.onDidChangeLocale).toBe('function');
|
|
||||||
expect(typeof api.ui.openSettings).toBe('function');
|
expect(typeof api.ui.openSettings).toBe('function');
|
||||||
expect(typeof api.capabilities.list).toBe('function');
|
expect(typeof api.capabilities.list).toBe('function');
|
||||||
expect(typeof api.commands.register).toBe('function');
|
expect(typeof api.commands.register).toBe('function');
|
||||||
|
|
@ -61,56 +58,6 @@ describe('VerstakPluginAPI contract', () => {
|
||||||
expect(permissionEnum).toContain('workbench.open');
|
expect(permissionEnum).toContain('workbench.open');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('manifest schema declares safe plugin localization catalogs', () => {
|
|
||||||
const localization = (manifestSchema as any).properties.localization;
|
|
||||||
|
|
||||||
expect(localization.type).toBe('object');
|
|
||||||
expect(localization.required).toEqual(['defaultLocale', 'locales']);
|
|
||||||
expect(localization.properties.defaultLocale.pattern).toBe('^[a-z]{2}(?:-[a-z0-9]+)*$');
|
|
||||||
expect(localization.properties.locales.propertyNames.pattern).toBe('^[a-z]{2}(?:-[a-z0-9]+)*$');
|
|
||||||
expect(localization.properties.locales.additionalProperties.pattern).toBe('^(?![\\\\/])(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('manifest types accept plugin-owned localization catalogs', () => {
|
|
||||||
const manifest: PluginManifest = {
|
|
||||||
schemaVersion: 1,
|
|
||||||
id: 'localized.plugin',
|
|
||||||
name: 'Localized Plugin',
|
|
||||||
version: '0.1.0',
|
|
||||||
apiVersion: '0.1.0',
|
|
||||||
provides: ['localized.example'],
|
|
||||||
permissions: ['ui.register'],
|
|
||||||
localization: {
|
|
||||||
defaultLocale: 'en',
|
|
||||||
locales: {
|
|
||||||
en: 'locales/en.json',
|
|
||||||
ru: 'locales/ru.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(manifest.localization?.defaultLocale).toBe('en');
|
|
||||||
expect(manifest.localization?.locales.ru).toBe('locales/ru.json');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mock i18n translates with fallback and named interpolation', () => {
|
|
||||||
const api = createMockPluginAPI('localized.plugin', {
|
|
||||||
locale: 'ru',
|
|
||||||
messages: {
|
|
||||||
en: { greeting: 'Hello, {name}!', onlyEnglish: 'English fallback' },
|
|
||||||
ru: { greeting: 'Привет, {name}!' },
|
|
||||||
},
|
|
||||||
defaultLocale: 'en',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(api.i18n.getLocale()).toBe('ru');
|
|
||||||
expect(api.i18n.t('greeting', { name: 'Мир' })).toBe('Привет, Мир!');
|
|
||||||
expect(api.i18n.t('onlyEnglish')).toBe('English fallback');
|
|
||||||
expect(api.i18n.t('missing', undefined, 'Fallback')).toBe('Fallback');
|
|
||||||
expect(api.i18n.t('unknown')).toBe('unknown');
|
|
||||||
expect(typeof api.i18n.onDidChangeLocale(() => {})).toBe('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('secrets capability and permissions are declared as dangerous platform contract', () => {
|
test('secrets capability and permissions are declared as dangerous platform contract', () => {
|
||||||
const capabilities = ((capabilitiesSchema as any).capabilities || []) as Array<{ name: string; status: string }>;
|
const capabilities = ((capabilitiesSchema as any).capabilities || []) as Array<{ name: string; status: string }>;
|
||||||
const permissions = ((permissionsSchema as any).permissions || []) as Array<{ name: string; dangerous: boolean }>;
|
const permissions = ((permissionsSchema as any).permissions || []) as Array<{ name: string; dangerous: boolean }>;
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import type {
|
||||||
|
|
||||||
export type PluginCommandArgs = Record<string, unknown>;
|
export type PluginCommandArgs = Record<string, unknown>;
|
||||||
export type PluginDataJSON = Record<string, unknown>;
|
export type PluginDataJSON = Record<string, unknown>;
|
||||||
export type PluginLocale = 'ru' | 'en';
|
|
||||||
export type TranslationParams = Record<string, string | number>;
|
|
||||||
export type PluginCommandHandler = (
|
export type PluginCommandHandler = (
|
||||||
args: PluginCommandArgs,
|
args: PluginCommandArgs,
|
||||||
declaration: PluginCommandDeclaration
|
declaration: PluginCommandDeclaration
|
||||||
|
|
@ -97,12 +95,6 @@ export interface BrowserReceiverPairing {
|
||||||
export interface VerstakPluginAPI {
|
export interface VerstakPluginAPI {
|
||||||
readonly pluginId: string;
|
readonly pluginId: string;
|
||||||
|
|
||||||
i18n: {
|
|
||||||
getLocale(): PluginLocale;
|
|
||||||
t(key: string, params?: TranslationParams, fallback?: string): string;
|
|
||||||
onDidChangeLocale(listener: (locale: PluginLocale) => void): Unsubscribe;
|
|
||||||
};
|
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
read(): Promise<PluginSettings>;
|
read(): Promise<PluginSettings>;
|
||||||
read<T = unknown>(key: string): Promise<T | undefined>;
|
read<T = unknown>(key: string): Promise<T | undefined>;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Verstak Plugin SDK — Test Utilities
|
// Verstak Plugin SDK — Test Utilities
|
||||||
|
|
||||||
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
import type { PluginManifest, PluginState, RegisteredContributionPoints } from './types';
|
||||||
import type { PluginCommandHandler, PluginLocale, TranslationParams, VerstakPluginAPI } from './plugin-api';
|
import type { PluginCommandHandler, VerstakPluginAPI } from './plugin-api';
|
||||||
|
|
||||||
const mockCommandHandlers = new Map<string, PluginCommandHandler>();
|
const mockCommandHandlers = new Map<string, PluginCommandHandler>();
|
||||||
|
|
||||||
|
|
@ -11,16 +11,6 @@ function commandKey(pluginId: string, commandId: string): string {
|
||||||
|
|
||||||
export interface MockPluginAPIOptions {
|
export interface MockPluginAPIOptions {
|
||||||
contributions?: RegisteredContributionPoints;
|
contributions?: RegisteredContributionPoints;
|
||||||
locale?: PluginLocale;
|
|
||||||
defaultLocale?: PluginLocale;
|
|
||||||
messages?: Partial<Record<PluginLocale, Record<string, string>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function interpolateMessage(message: string, params?: TranslationParams): string {
|
|
||||||
if (!params) return message;
|
|
||||||
return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, (placeholder, name: string) => (
|
|
||||||
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : placeholder
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,9 +51,6 @@ export function createTestPluginState(overrides?: Partial<PluginState>): PluginS
|
||||||
* Создать заглушку VerstakPluginAPI для тестов.
|
* Создать заглушку VerstakPluginAPI для тестов.
|
||||||
*/
|
*/
|
||||||
export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPluginAPIOptions = {}): VerstakPluginAPI {
|
export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPluginAPIOptions = {}): VerstakPluginAPI {
|
||||||
const locale = options.locale || 'en';
|
|
||||||
const defaultLocale = options.defaultLocale || 'en';
|
|
||||||
const messages = options.messages || {};
|
|
||||||
const settings: Record<string, unknown> = {};
|
const settings: Record<string, unknown> = {};
|
||||||
const pluginData = new Map<string, Record<string, unknown>>();
|
const pluginData = new Map<string, Record<string, unknown>>();
|
||||||
const commands = new Map<string, PluginCommandHandler>();
|
const commands = new Map<string, PluginCommandHandler>();
|
||||||
|
|
@ -152,17 +139,6 @@ export function createMockPluginAPI(pluginId = 'test.plugin', options: MockPlugi
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pluginId,
|
pluginId,
|
||||||
i18n: {
|
|
||||||
getLocale: vi.fn(() => locale),
|
|
||||||
t: vi.fn((key: string, params?: TranslationParams, fallback?: string) => {
|
|
||||||
const message = messages[locale]?.[key]
|
|
||||||
?? messages[defaultLocale]?.[key]
|
|
||||||
?? fallback
|
|
||||||
?? key;
|
|
||||||
return interpolateMessage(message, params);
|
|
||||||
}),
|
|
||||||
onDidChangeLocale: vi.fn((_listener: (nextLocale: PluginLocale) => void) => () => {}),
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
read: vi.fn(async (key?: string) => key ? settings[key] : { ...settings }) as VerstakPluginAPI['settings']['read'],
|
read: vi.fn(async (key?: string) => key ? settings[key] : { ...settings }) as VerstakPluginAPI['settings']['read'],
|
||||||
write: vi.fn(async (key: string, value: unknown) => {
|
write: vi.fn(async (key: string, value: unknown) => {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,6 @@
|
||||||
|
|
||||||
export type PluginSource = 'official' | 'local' | 'third-party';
|
export type PluginSource = 'official' | 'local' | 'third-party';
|
||||||
|
|
||||||
export interface PluginLocalizationConfig {
|
|
||||||
defaultLocale: string;
|
|
||||||
locales: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginManifest {
|
export interface PluginManifest {
|
||||||
schemaVersion: 1;
|
schemaVersion: 1;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -18,7 +13,6 @@ export interface PluginManifest {
|
||||||
description?: string;
|
description?: string;
|
||||||
source?: PluginSource;
|
source?: PluginSource;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
localization?: PluginLocalizationConfig;
|
|
||||||
provides: string[];
|
provides: string[];
|
||||||
requires?: string[];
|
requires?: string[];
|
||||||
optionalRequires?: string[];
|
optionalRequires?: string[];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue