@vestra-player/sdk 1.5.25 → 1.5.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/bin/vestra-plugin.mjs +85 -10
- package/dist/api-requests.d.ts +36 -0
- package/dist/api-requests.js +143 -0
- package/dist/capabilities.d.ts +6 -3
- package/dist/capabilities.js +16 -230
- package/dist/capability-check.d.ts +85 -0
- package/dist/capability-check.js +203 -0
- package/dist/capability-rules.d.ts +102 -0
- package/dist/capability-rules.js +132 -0
- package/dist/host-capabilities.d.ts +10 -0
- package/dist/host-capabilities.js +360 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/types.d.ts +2 -1
- package/dist/ts-plugin.d.ts +23 -0
- package/dist/ts-plugin.js +135 -0
- package/llms-full.txt +51 -6
- package/llms.txt +23 -23
- package/package.json +9 -3
- package/ts-plugin/index.d.ts +8 -0
- package/ts-plugin/index.js +6 -0
- package/ts-plugin/package.json +3 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability checker: every use of a plugin API member whose capability
|
|
3
|
+
* the plugin's `manifest.json` does not declare. `vestra-plugin check` (and
|
|
4
|
+
* so `build` and `pack`) and the TypeScript language-service plugin
|
|
5
|
+
* (`@vestra-player/sdk/ts-plugin`) both run it; the rule is
|
|
6
|
+
* `capability-rules.ts` (the player's table and the SDK's requests).
|
|
7
|
+
*
|
|
8
|
+
* It is type-aware: a use is a property access, an element access with a
|
|
9
|
+
* literal name or a destructured property whose symbol is the SDK's
|
|
10
|
+
* declaration of a member in the table, however the object was reached
|
|
11
|
+
* (`plugin.player.seek`, `const { player } = plugin; player.seek`, a
|
|
12
|
+
* parameter typed `Player`, a `Panel` from `panels.mount`). Not detected:
|
|
13
|
+
* access with a computed name (`player[name]`), values typed `any` or
|
|
14
|
+
* `unknown`, plain JavaScript without types, members inherited from `View`
|
|
15
|
+
* (their object comes from a call that is checked) and options that need
|
|
16
|
+
* more (a `processed` capture, a file resolver).
|
|
17
|
+
*
|
|
18
|
+
* The TypeScript module is passed in, never imported: the CLI uses the
|
|
19
|
+
* SDK's own compiler, the editor its TypeScript server's.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
import type TS from "typescript";
|
|
24
|
+
import type { CapabilityRules } from "./capability-rules.ts";
|
|
25
|
+
/** @internal The module plugins import the API types from. */
|
|
26
|
+
export declare const SDK_MODULE = "@vestra-player/sdk";
|
|
27
|
+
/** @internal The diagnostic code the editor shows (and its quick fix keys on). */
|
|
28
|
+
export declare const CAPABILITY_DIAGNOSTIC = 95301;
|
|
29
|
+
/** @internal One use of a member whose capability is not declared. */
|
|
30
|
+
export interface CapabilityFinding {
|
|
31
|
+
/** The source file, as the program names it. */
|
|
32
|
+
readonly file: string;
|
|
33
|
+
/** 1-based line and column of the member's name. */
|
|
34
|
+
readonly line: number;
|
|
35
|
+
readonly column: number;
|
|
36
|
+
/** Offset and length of the member's name in the file. */
|
|
37
|
+
readonly start: number;
|
|
38
|
+
readonly length: number;
|
|
39
|
+
/** The member, `Interface.member` (`Player.seek`). */
|
|
40
|
+
readonly api: string;
|
|
41
|
+
/** The capability to declare. */
|
|
42
|
+
readonly capability: string;
|
|
43
|
+
/** The request that needs it. */
|
|
44
|
+
readonly method: string;
|
|
45
|
+
/** The declared-or-needed capability whose `requires` names it. */
|
|
46
|
+
readonly requiredBy?: string;
|
|
47
|
+
/** One line for people. */
|
|
48
|
+
readonly message: string;
|
|
49
|
+
}
|
|
50
|
+
/** @internal */
|
|
51
|
+
export interface CheckOptions {
|
|
52
|
+
/** The files to check (others in the program are only read for types). */
|
|
53
|
+
readonly files: readonly string[];
|
|
54
|
+
/** The manifest's `capabilities`. */
|
|
55
|
+
readonly capabilities: readonly string[];
|
|
56
|
+
/** Default: the SDK's rules. */
|
|
57
|
+
readonly rules?: CapabilityRules;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Every use, in `options.files`, of a member whose capability the manifest
|
|
61
|
+
* does not declare, in file and position order.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export declare function findUndeclaredCapabilities(ts: typeof TS, program: TS.Program, options: CheckOptions): CapabilityFinding[];
|
|
66
|
+
/**
|
|
67
|
+
* Whether a file belongs to the plugin's production code, given the
|
|
68
|
+
* directory of its `manifest.json`: not in `tests/`, `node_modules/`,
|
|
69
|
+
* `dist/`, `vendor/` or a dot directory (what `vestra-plugin build` leaves
|
|
70
|
+
* out), and not a declaration file.
|
|
71
|
+
*
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export declare function isPluginSource(directory: string, file: string): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The edit that adds a capability to a manifest's `capabilities` array, in
|
|
77
|
+
* the array's own layout; `undefined` when the text has no such array.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare function addCapabilityEdit(ts: typeof TS, text: string, capability: string): {
|
|
82
|
+
start: number;
|
|
83
|
+
length: number;
|
|
84
|
+
newText: string;
|
|
85
|
+
} | undefined;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { memberNeeds, RULES } from "./capability-rules.js";
|
|
2
|
+
/** @internal The module plugins import the API types from. */
|
|
3
|
+
export const SDK_MODULE = "@vestra-player/sdk";
|
|
4
|
+
/** @internal The diagnostic code the editor shows (and its quick fix keys on). */
|
|
5
|
+
export const CAPABILITY_DIAGNOSTIC = 95_301;
|
|
6
|
+
const memberCache = new WeakMap();
|
|
7
|
+
/**
|
|
8
|
+
* The module `@vestra-player/sdk` as the program resolved it: from the
|
|
9
|
+
* first import or export of it in the program's own (non-declaration) files.
|
|
10
|
+
*/
|
|
11
|
+
function sdkModule(ts, program, checker) {
|
|
12
|
+
for (const file of program.getSourceFiles()) {
|
|
13
|
+
if (file.isDeclarationFile || program.isSourceFileFromExternalLibrary(file))
|
|
14
|
+
continue;
|
|
15
|
+
for (const statement of file.statements) {
|
|
16
|
+
const specifier = ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)
|
|
17
|
+
? statement.moduleSpecifier
|
|
18
|
+
: undefined;
|
|
19
|
+
if (specifier &&
|
|
20
|
+
ts.isStringLiteral(specifier) &&
|
|
21
|
+
specifier.text === SDK_MODULE) {
|
|
22
|
+
const module = checker.getSymbolAtLocation(specifier);
|
|
23
|
+
if (module)
|
|
24
|
+
return module;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
/** The SDK's declarations of the members in the table. */
|
|
31
|
+
function sdkMembers(ts, program, rules) {
|
|
32
|
+
const cached = memberCache.get(program);
|
|
33
|
+
if (cached && rules === RULES)
|
|
34
|
+
return cached;
|
|
35
|
+
const members = new Map();
|
|
36
|
+
const checker = program.getTypeChecker();
|
|
37
|
+
const module = sdkModule(ts, program, checker);
|
|
38
|
+
if (module) {
|
|
39
|
+
const exports = new Map();
|
|
40
|
+
for (const symbol of checker.getExportsOfModule(module))
|
|
41
|
+
exports.set(symbol.name, symbol.flags & ts.SymbolFlags.Alias
|
|
42
|
+
? checker.getAliasedSymbol(symbol)
|
|
43
|
+
: symbol);
|
|
44
|
+
for (const api of Object.keys(rules.apis)) {
|
|
45
|
+
const [head, ...path] = api.split(".");
|
|
46
|
+
const root = exports.get(head);
|
|
47
|
+
if (!root || !path.length)
|
|
48
|
+
continue;
|
|
49
|
+
let type = checker.getDeclaredTypeOfSymbol(root);
|
|
50
|
+
let property;
|
|
51
|
+
for (const name of path) {
|
|
52
|
+
property = type?.getProperty(name);
|
|
53
|
+
type = property && checker.getTypeOfSymbol(property);
|
|
54
|
+
}
|
|
55
|
+
for (const declaration of property?.declarations ?? [])
|
|
56
|
+
if (!members.has(declaration))
|
|
57
|
+
members.set(declaration, api);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (rules === RULES)
|
|
61
|
+
memberCache.set(program, members);
|
|
62
|
+
return members;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every use, in `options.files`, of a member whose capability the manifest
|
|
66
|
+
* does not declare, in file and position order.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
export function findUndeclaredCapabilities(ts, program, options) {
|
|
71
|
+
const rules = options.rules ?? RULES;
|
|
72
|
+
const files = options.files
|
|
73
|
+
.map((file) => program.getSourceFile(file))
|
|
74
|
+
.filter((file) => !!file);
|
|
75
|
+
if (!files.length)
|
|
76
|
+
return [];
|
|
77
|
+
const members = sdkMembers(ts, program, rules);
|
|
78
|
+
if (!members.size)
|
|
79
|
+
return [];
|
|
80
|
+
const checker = program.getTypeChecker();
|
|
81
|
+
const findings = [];
|
|
82
|
+
const memberOf = (symbol) => {
|
|
83
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
84
|
+
const api = members.get(declaration);
|
|
85
|
+
if (api)
|
|
86
|
+
return api;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const report = (name, symbol) => {
|
|
92
|
+
const api = memberOf(symbol);
|
|
93
|
+
if (!api)
|
|
94
|
+
return;
|
|
95
|
+
const start = name.getStart(file);
|
|
96
|
+
const { line, character } = file.getLineAndCharacterOfPosition(start);
|
|
97
|
+
for (const need of memberNeeds(api, options.capabilities, rules) ?? []) {
|
|
98
|
+
if (options.capabilities.includes(need.capability))
|
|
99
|
+
continue;
|
|
100
|
+
findings.push({
|
|
101
|
+
file: file.fileName,
|
|
102
|
+
line: line + 1,
|
|
103
|
+
column: character + 1,
|
|
104
|
+
start,
|
|
105
|
+
length: name.getEnd() - start,
|
|
106
|
+
api,
|
|
107
|
+
capability: need.capability,
|
|
108
|
+
method: need.method,
|
|
109
|
+
...(need.requiredBy ? { requiredBy: need.requiredBy } : {}),
|
|
110
|
+
message: need.requiredBy
|
|
111
|
+
? `${api} needs the capability "${need.capability}" (required by "${need.requiredBy}"), which manifest.json does not declare`
|
|
112
|
+
: `${api} needs the capability "${need.capability}", which manifest.json does not declare`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const visit = (node) => {
|
|
117
|
+
if (ts.isPropertyAccessExpression(node))
|
|
118
|
+
report(node.name, checker.getSymbolAtLocation(node.name));
|
|
119
|
+
else if (ts.isElementAccessExpression(node) &&
|
|
120
|
+
ts.isStringLiteralLike(node.argumentExpression))
|
|
121
|
+
report(node.argumentExpression, checker.getSymbolAtLocation(node.argumentExpression));
|
|
122
|
+
else if (ts.isBindingElement(node) &&
|
|
123
|
+
ts.isObjectBindingPattern(node.parent)) {
|
|
124
|
+
const name = node.propertyName ?? node.name;
|
|
125
|
+
const text = ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
126
|
+
? name.text
|
|
127
|
+
: undefined;
|
|
128
|
+
if (text !== undefined)
|
|
129
|
+
report(name, checker.getTypeAtLocation(node.parent).getProperty(text));
|
|
130
|
+
}
|
|
131
|
+
ts.forEachChild(node, visit);
|
|
132
|
+
};
|
|
133
|
+
visit(file);
|
|
134
|
+
}
|
|
135
|
+
return findings;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether a file belongs to the plugin's production code, given the
|
|
139
|
+
* directory of its `manifest.json`: not in `tests/`, `node_modules/`,
|
|
140
|
+
* `dist/`, `vendor/` or a dot directory (what `vestra-plugin build` leaves
|
|
141
|
+
* out), and not a declaration file.
|
|
142
|
+
*
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
145
|
+
export function isPluginSource(directory, file) {
|
|
146
|
+
const normal = (path) => path.replaceAll("\\", "/").replace(/\/$/, "");
|
|
147
|
+
const base = normal(directory);
|
|
148
|
+
const path = normal(file);
|
|
149
|
+
const caseless = /^[a-z]:\//i.test(path);
|
|
150
|
+
const same = (a, b) => caseless ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
151
|
+
if (!same(path.slice(0, base.length + 1), `${base}/`))
|
|
152
|
+
return false;
|
|
153
|
+
if (/\.d\.[mc]?ts$/.test(path) || !/\.[mc]?tsx?$/.test(path))
|
|
154
|
+
return false;
|
|
155
|
+
const parts = path
|
|
156
|
+
.slice(base.length + 1)
|
|
157
|
+
.split("/")
|
|
158
|
+
.slice(0, -1);
|
|
159
|
+
return !parts.some((part) => part.startsWith(".") ||
|
|
160
|
+
["tests", "node_modules", "dist", "vendor"].includes(part));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The edit that adds a capability to a manifest's `capabilities` array, in
|
|
164
|
+
* the array's own layout; `undefined` when the text has no such array.
|
|
165
|
+
*
|
|
166
|
+
* @internal
|
|
167
|
+
*/
|
|
168
|
+
export function addCapabilityEdit(ts, text, capability) {
|
|
169
|
+
const json = ts.parseJsonText("manifest.json", text);
|
|
170
|
+
const root = json.statements[0]?.expression;
|
|
171
|
+
if (!root || !ts.isObjectLiteralExpression(root))
|
|
172
|
+
return undefined;
|
|
173
|
+
const property = root.properties.find((each) => ts.isPropertyAssignment(each) &&
|
|
174
|
+
ts.isStringLiteral(each.name) &&
|
|
175
|
+
each.name.text === "capabilities");
|
|
176
|
+
const array = property?.initializer;
|
|
177
|
+
if (!array || !ts.isArrayLiteralExpression(array))
|
|
178
|
+
return undefined;
|
|
179
|
+
const quoted = JSON.stringify(capability);
|
|
180
|
+
const last = array.elements.at(-1);
|
|
181
|
+
if (!last) {
|
|
182
|
+
const start = array.getStart(json);
|
|
183
|
+
return {
|
|
184
|
+
start,
|
|
185
|
+
length: array.getEnd() - start,
|
|
186
|
+
newText: `[${quoted}]`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const first = array.elements[0];
|
|
190
|
+
const lineStart = text.lastIndexOf("\n", first.getStart(json)) + 1;
|
|
191
|
+
const multiline = text
|
|
192
|
+
.slice(array.getStart(json), first.getStart(json))
|
|
193
|
+
.includes("\n");
|
|
194
|
+
const indent = text.slice(lineStart, first.getStart(json));
|
|
195
|
+
return {
|
|
196
|
+
start: last.getEnd(),
|
|
197
|
+
length: 0,
|
|
198
|
+
// The manifest's own line endings (CRLF on many Windows checkouts).
|
|
199
|
+
newText: multiline
|
|
200
|
+
? `,${text.includes("\r\n") ? "\r\n" : "\n"}${indent}${quoted}`
|
|
201
|
+
: `, ${quoted}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ApiRequest } from "./api-requests.ts";
|
|
2
|
+
/**
|
|
3
|
+
* What a request needs, as the player's `capabilities.json` writes it:
|
|
4
|
+
* `null` (nothing), a capability name, a choice by a string parameter, or a
|
|
5
|
+
* choice by whether the plugin declared a capability.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export type Needs = null | string | {
|
|
10
|
+
readonly param: string;
|
|
11
|
+
readonly cases: Readonly<Record<string, string | null>>;
|
|
12
|
+
readonly otherwise: string | null;
|
|
13
|
+
} | {
|
|
14
|
+
readonly ifDeclared: string;
|
|
15
|
+
readonly then: string;
|
|
16
|
+
readonly otherwise: string;
|
|
17
|
+
};
|
|
18
|
+
/** @internal One RPC method of the player's table (`prefix.*` covers a prefix). */
|
|
19
|
+
export interface HostMethod {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly needs: Needs;
|
|
22
|
+
}
|
|
23
|
+
/** @internal One manifest capability of the player's table. */
|
|
24
|
+
export interface HostCapability {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly risky: boolean;
|
|
28
|
+
readonly requires: readonly string[];
|
|
29
|
+
readonly residency: string;
|
|
30
|
+
readonly methods: readonly HostMethod[];
|
|
31
|
+
}
|
|
32
|
+
/** @internal The player's `src/host/capabilities.json`. */
|
|
33
|
+
export interface HostCapabilities {
|
|
34
|
+
readonly about: string;
|
|
35
|
+
readonly apiVersion: string;
|
|
36
|
+
readonly capabilities: readonly HostCapability[];
|
|
37
|
+
readonly uncapable: readonly HostMethod[];
|
|
38
|
+
}
|
|
39
|
+
/** @internal Both tables; tests pass their own. */
|
|
40
|
+
export interface CapabilityRules {
|
|
41
|
+
readonly host: HostCapabilities;
|
|
42
|
+
readonly apis: Readonly<Record<string, readonly ApiRequest[]>>;
|
|
43
|
+
}
|
|
44
|
+
/** @internal The SDK's rules: the synced player table and `API_REQUESTS`. */
|
|
45
|
+
export declare const RULES: CapabilityRules;
|
|
46
|
+
/**
|
|
47
|
+
* The player's entry for a method, as the player routes it: the exact name
|
|
48
|
+
* first, then the first `prefix.*` entry whose prefix it starts with.
|
|
49
|
+
* `undefined`: the player does not know the method.
|
|
50
|
+
*
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export declare function hostMethod(method: string, host?: HostCapabilities): HostMethod | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* The capability one request needs, as the player decides it
|
|
56
|
+
* (`capabilities::Route::needs`): `null` for none, `undefined` for a method
|
|
57
|
+
* the player does not know.
|
|
58
|
+
*
|
|
59
|
+
* @param method - The wire method.
|
|
60
|
+
* @param params - Its string parameters that a `Needs::Param` rule reads.
|
|
61
|
+
* @param declared - Whether the plugin declared a capability.
|
|
62
|
+
*
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
export declare function requestNeeds(method: string, params: Readonly<Record<string, string>> | undefined, declared: (capability: string) => boolean, host?: HostCapabilities): string | null | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* One capability a member needs: the request that needs it, and the
|
|
68
|
+
* capability whose `requires` brought it in (absent when the request needs
|
|
69
|
+
* it directly).
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export interface MemberNeed {
|
|
74
|
+
readonly capability: string;
|
|
75
|
+
readonly method: string;
|
|
76
|
+
readonly requiredBy?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every capability a member of the plugin API needs with the given manifest:
|
|
80
|
+
* those its requests need and, through `requires`, those they need in turn.
|
|
81
|
+
* `undefined`: the member is not in the table (it needs none).
|
|
82
|
+
*
|
|
83
|
+
* @param api - The member as `Interface.member` (`Player.seek`).
|
|
84
|
+
* @param capabilities - The manifest's capabilities.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
export declare function memberNeeds(api: string, capabilities: readonly string[], rules?: CapabilityRules): MemberNeed[] | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* `CAPABILITY_INFO` from the tables: the label, highlight and requirements
|
|
91
|
+
* from the player's table; the members from `API_REQUESTS` (every member a
|
|
92
|
+
* request of which needs the capability, in the table's order; requests
|
|
93
|
+
* marked `also` are left out, as they are documented at the member).
|
|
94
|
+
*
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export declare function capabilityInfo(rules?: CapabilityRules): Record<string, {
|
|
98
|
+
apis: string[];
|
|
99
|
+
requires: string[];
|
|
100
|
+
label: string;
|
|
101
|
+
risky: boolean;
|
|
102
|
+
}>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability rule, as data: which manifest capability a request needs
|
|
3
|
+
* (the player's table, `HOST_CAPABILITIES`, synced from its
|
|
4
|
+
* `src/host/capabilities.json`) and which requests each public member of the
|
|
5
|
+
* plugin API sends (`API_REQUESTS`). `CAPABILITY_INFO`, `vestra-plugin check`
|
|
6
|
+
* and the TypeScript language-service plugin are all built from these two
|
|
7
|
+
* tables, so they and the player cannot disagree.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
import { API_REQUESTS } from "./api-requests.js";
|
|
12
|
+
import { HOST_CAPABILITIES } from "./host-capabilities.js";
|
|
13
|
+
/** @internal The SDK's rules: the synced player table and `API_REQUESTS`. */
|
|
14
|
+
export const RULES = {
|
|
15
|
+
host: HOST_CAPABILITIES,
|
|
16
|
+
apis: API_REQUESTS,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The player's entry for a method, as the player routes it: the exact name
|
|
20
|
+
* first, then the first `prefix.*` entry whose prefix it starts with.
|
|
21
|
+
* `undefined`: the player does not know the method.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function hostMethod(method, host = RULES.host) {
|
|
26
|
+
const all = [
|
|
27
|
+
...host.capabilities.flatMap((capability) => capability.methods),
|
|
28
|
+
...host.uncapable,
|
|
29
|
+
];
|
|
30
|
+
return (all.find((entry) => entry.name === method) ??
|
|
31
|
+
all.find((entry) => entry.name.endsWith(".*") && method.startsWith(entry.name.slice(0, -1))));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The capability one request needs, as the player decides it
|
|
35
|
+
* (`capabilities::Route::needs`): `null` for none, `undefined` for a method
|
|
36
|
+
* the player does not know.
|
|
37
|
+
*
|
|
38
|
+
* @param method - The wire method.
|
|
39
|
+
* @param params - Its string parameters that a `Needs::Param` rule reads.
|
|
40
|
+
* @param declared - Whether the plugin declared a capability.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export function requestNeeds(method, params, declared, host = RULES.host) {
|
|
45
|
+
const entry = hostMethod(method, host);
|
|
46
|
+
if (!entry)
|
|
47
|
+
return undefined;
|
|
48
|
+
const needs = entry.needs;
|
|
49
|
+
if (needs === null || typeof needs === "string")
|
|
50
|
+
return needs;
|
|
51
|
+
if ("param" in needs) {
|
|
52
|
+
const value = params?.[needs.param];
|
|
53
|
+
return value !== undefined && Object.hasOwn(needs.cases, value)
|
|
54
|
+
? needs.cases[value]
|
|
55
|
+
: needs.otherwise;
|
|
56
|
+
}
|
|
57
|
+
return declared(needs.ifDeclared) ? needs.then : needs.otherwise;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Every capability a member of the plugin API needs with the given manifest:
|
|
61
|
+
* those its requests need and, through `requires`, those they need in turn.
|
|
62
|
+
* `undefined`: the member is not in the table (it needs none).
|
|
63
|
+
*
|
|
64
|
+
* @param api - The member as `Interface.member` (`Player.seek`).
|
|
65
|
+
* @param capabilities - The manifest's capabilities.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export function memberNeeds(api, capabilities, rules = RULES) {
|
|
70
|
+
const requests = rules.apis[api];
|
|
71
|
+
if (!requests)
|
|
72
|
+
return undefined;
|
|
73
|
+
// An `ifDeclared` rule is decided last, as if the plugin had declared what
|
|
74
|
+
// the member's other requests need: `Player.seek` needs `playback.control`
|
|
75
|
+
// for `source.submit`, and with it `source.operation` needs nothing more.
|
|
76
|
+
const declared = (name) => capabilities.includes(name) ||
|
|
77
|
+
needs.some((need) => need.capability === name);
|
|
78
|
+
const conditional = (request) => {
|
|
79
|
+
const rule = hostMethod(request.method, rules.host)?.needs;
|
|
80
|
+
return typeof rule === "object" && rule !== null && "ifDeclared" in rule;
|
|
81
|
+
};
|
|
82
|
+
const needs = [];
|
|
83
|
+
const add = (need) => {
|
|
84
|
+
if (needs.some((each) => each.capability === need.capability))
|
|
85
|
+
return;
|
|
86
|
+
needs.push(need);
|
|
87
|
+
const entry = rules.host.capabilities.find((capability) => capability.name === need.capability);
|
|
88
|
+
for (const required of entry?.requires ?? [])
|
|
89
|
+
add({
|
|
90
|
+
capability: required,
|
|
91
|
+
method: need.method,
|
|
92
|
+
requiredBy: need.capability,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
for (const request of [
|
|
96
|
+
...requests.filter((request) => !conditional(request)),
|
|
97
|
+
...requests.filter(conditional),
|
|
98
|
+
]) {
|
|
99
|
+
const capability = requestNeeds(request.method, request.params, declared, rules.host);
|
|
100
|
+
if (capability)
|
|
101
|
+
add({ capability, method: request.method });
|
|
102
|
+
}
|
|
103
|
+
return needs;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `CAPABILITY_INFO` from the tables: the label, highlight and requirements
|
|
107
|
+
* from the player's table; the members from `API_REQUESTS` (every member a
|
|
108
|
+
* request of which needs the capability, in the table's order; requests
|
|
109
|
+
* marked `also` are left out, as they are documented at the member).
|
|
110
|
+
*
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
export function capabilityInfo(rules = RULES) {
|
|
114
|
+
const info = {};
|
|
115
|
+
for (const capability of rules.host.capabilities)
|
|
116
|
+
info[capability.name] = {
|
|
117
|
+
apis: [],
|
|
118
|
+
requires: [...capability.requires],
|
|
119
|
+
label: capability.label,
|
|
120
|
+
risky: capability.risky,
|
|
121
|
+
};
|
|
122
|
+
for (const [api, requests] of Object.entries(rules.apis))
|
|
123
|
+
for (const request of requests) {
|
|
124
|
+
if (request.also)
|
|
125
|
+
continue;
|
|
126
|
+
const capability = requestNeeds(request.method, request.params, () => true, rules.host);
|
|
127
|
+
const entry = capability ? info[capability] : undefined;
|
|
128
|
+
if (entry && !entry.apis.includes(api))
|
|
129
|
+
entry.apis.push(api);
|
|
130
|
+
}
|
|
131
|
+
return info;
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The player's capability table, copied from its `src/host/capabilities.json`
|
|
3
|
+
* by `npm run capabilities:sync -- <player repository>`. Do not edit: change
|
|
4
|
+
* the player's capability modules, regenerate its JSON and sync again.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
import type { HostCapabilities } from "./capability-rules.ts";
|
|
9
|
+
/** @internal */
|
|
10
|
+
export declare const HOST_CAPABILITIES: HostCapabilities;
|