@vestra-player/sdk 1.5.24 → 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 +19 -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 +2 -1
- package/dist/player-runtime.js +2 -1
- package/dist/plugin/services.d.ts +35 -0
- package/dist/plugin/services.js +87 -2
- package/dist/plugin/testing.d.ts +24 -1
- package/dist/plugin/testing.js +71 -1
- package/dist/plugin/types.d.ts +79 -1
- package/dist/ts-plugin.d.ts +23 -0
- package/dist/ts-plugin.js +135 -0
- package/dist/wire.d.ts +12 -0
- package/dist/wire.js +10 -0
- package/llms-full.txt +201 -8
- 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
package/dist/capabilities.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { capabilityInfo } from "./capability-rules.js";
|
|
1
2
|
/**
|
|
2
3
|
* Every manifest capability the player knows (`vestra/1`), in the player's
|
|
3
4
|
* order, with the members it opens, the capabilities they also need and the
|
|
4
|
-
* store's wording.
|
|
5
|
-
*
|
|
5
|
+
* store's wording. Built from the player's own capability table (its
|
|
6
|
+
* `src/host/capabilities.json`, copied into the SDK) and the requests each
|
|
7
|
+
* member sends, the same rule `vestra-plugin check` and the SDK's
|
|
8
|
+
* TypeScript plugin apply; the player's tests check that the copy is
|
|
9
|
+
* current.
|
|
6
10
|
*
|
|
7
11
|
* A member not listed under any capability needs none (`plugin.tools`,
|
|
8
12
|
* `plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui`,
|
|
@@ -21,237 +25,19 @@
|
|
|
21
25
|
*
|
|
22
26
|
* @since 1.5.14
|
|
23
27
|
*/
|
|
24
|
-
export const CAPABILITY_INFO = Object.freeze(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"Player.state",
|
|
34
|
-
"Player.refresh",
|
|
35
|
-
"Player.stats",
|
|
36
|
-
"Player.chapters",
|
|
37
|
-
"Player.audioDevices",
|
|
38
|
-
"Timeline.marks",
|
|
39
|
-
],
|
|
40
|
-
requires: [],
|
|
41
|
-
label: "Read what is playing and the position",
|
|
42
|
-
risky: false,
|
|
43
|
-
},
|
|
44
|
-
"playback.control": {
|
|
45
|
-
apis: [
|
|
46
|
-
"Player.open",
|
|
47
|
-
"Player.setPaused",
|
|
48
|
-
"Player.seek",
|
|
49
|
-
"Player.setRate",
|
|
50
|
-
"Player.restart",
|
|
51
|
-
"Player.close",
|
|
52
|
-
"Player.scrub",
|
|
53
|
-
"Player.setOutput",
|
|
54
|
-
"Player.provideSequence",
|
|
55
|
-
"Player.next",
|
|
56
|
-
"Player.previous",
|
|
57
|
-
"Player.addAudioTrack",
|
|
58
|
-
"Player.selectAudioTrack",
|
|
59
|
-
"Player.step",
|
|
60
|
-
"Player.setStepSeek",
|
|
61
|
-
"Player.setLoop",
|
|
62
|
-
"Player.setAudioDevice",
|
|
63
|
-
"Power.preventSleep",
|
|
64
|
-
],
|
|
65
|
-
requires: [],
|
|
66
|
-
label: "Open media, control playback, keep the computer awake",
|
|
67
|
-
risky: false,
|
|
68
|
-
},
|
|
69
|
-
"source.resolve": {
|
|
70
|
-
apis: ["Resolvers.add"],
|
|
71
|
-
requires: [],
|
|
72
|
-
label: "Resolve online video addresses",
|
|
73
|
-
risky: false,
|
|
74
|
-
},
|
|
75
|
-
events: {
|
|
76
|
-
apis: [
|
|
77
|
-
"Player.onChange",
|
|
78
|
-
"Subtitles.onChange",
|
|
79
|
-
"Timeline.onChange",
|
|
80
|
-
"MainWindow.onChange",
|
|
81
|
-
"Appearance.onChange",
|
|
82
|
-
"PanelSlot.onChange",
|
|
83
|
-
"Panel.onChange",
|
|
84
|
-
"BackdropSlot.onChange",
|
|
85
|
-
"Resolvers.onFileTypesChange",
|
|
86
|
-
],
|
|
87
|
-
requires: [],
|
|
88
|
-
label: "Receive playback changes as they happen",
|
|
89
|
-
risky: false,
|
|
90
|
-
},
|
|
91
|
-
commands: {
|
|
92
|
-
apis: ["Commands.add", "Host.run"],
|
|
93
|
-
requires: [],
|
|
94
|
-
label: "Add menu commands and shortcuts",
|
|
95
|
-
risky: false,
|
|
96
|
-
},
|
|
97
|
-
"ui.window": {
|
|
98
|
-
apis: ["Windows.create"],
|
|
99
|
-
requires: [],
|
|
100
|
-
label: "Open tool windows",
|
|
101
|
-
risky: false,
|
|
102
|
-
},
|
|
103
|
-
"ui.mount": {
|
|
104
|
-
apis: [
|
|
105
|
-
"Panels.mount",
|
|
106
|
-
"Panels.bottom",
|
|
107
|
-
"Panels.sidebar",
|
|
108
|
-
"Panels.backdrop",
|
|
109
|
-
"PanelSlot.onChange",
|
|
110
|
-
"Panel.onChange",
|
|
111
|
-
"BackdropSlot.onChange",
|
|
112
|
-
],
|
|
113
|
-
requires: [],
|
|
114
|
-
label: "Show panels in the bottom bar, the sidebar or behind the video",
|
|
115
|
-
risky: false,
|
|
116
|
-
},
|
|
117
|
-
"ui.dialog": {
|
|
118
|
-
apis: [
|
|
119
|
-
"Dialogs.confirm",
|
|
120
|
-
"Dialogs.openFile",
|
|
121
|
-
"Dialogs.openFiles",
|
|
122
|
-
"Dialogs.openDirectory",
|
|
123
|
-
"Dialogs.saveFile",
|
|
124
|
-
],
|
|
125
|
-
requires: [],
|
|
126
|
-
label: "Show file pickers and confirmations",
|
|
127
|
-
risky: false,
|
|
128
|
-
},
|
|
129
|
-
"plugins.manage": {
|
|
130
|
-
apis: [
|
|
131
|
-
"Packages.validate",
|
|
132
|
-
"Packages.validateArchive",
|
|
133
|
-
"Packages.setEnabled",
|
|
134
|
-
"Packages.reload",
|
|
135
|
-
"Packages.uninstall",
|
|
136
|
-
],
|
|
137
|
-
requires: [],
|
|
138
|
-
label: "Install, uninstall and disable other plugins",
|
|
139
|
-
risky: true,
|
|
140
|
-
},
|
|
141
|
-
"processing.video": {
|
|
142
|
-
apis: ["Plugin.video.filter", "Plugin.video.view", "Plugin.video.render"],
|
|
143
|
-
requires: ["playback.read"],
|
|
144
|
-
label: "Process the picture (filters, shaders)",
|
|
145
|
-
risky: false,
|
|
146
|
-
},
|
|
147
|
-
"processing.audio": {
|
|
148
|
-
apis: ["Plugin.audio.filter"],
|
|
149
|
-
requires: ["playback.read", "events"],
|
|
150
|
-
label: "Process the sound",
|
|
151
|
-
risky: false,
|
|
152
|
-
},
|
|
153
|
-
"subtitles.read": {
|
|
154
|
-
apis: ["Subtitles.state", "Subtitles.onChange", "Subtitles.refresh"],
|
|
155
|
-
requires: [],
|
|
156
|
-
label: "Read subtitles",
|
|
157
|
-
risky: false,
|
|
158
|
-
},
|
|
159
|
-
"subtitles.control": {
|
|
160
|
-
apis: [
|
|
161
|
-
"Subtitles.select",
|
|
162
|
-
"Subtitles.load",
|
|
163
|
-
"Subtitles.remove",
|
|
164
|
-
"Subtitles.setStyle",
|
|
165
|
-
],
|
|
166
|
-
requires: [],
|
|
167
|
-
label: "Load and change subtitles",
|
|
168
|
-
risky: false,
|
|
169
|
-
},
|
|
170
|
-
"media.read": {
|
|
171
|
-
apis: ["MediaCapture.capture", "Snapshot.export"],
|
|
172
|
-
requires: ["media.control"],
|
|
173
|
-
label: "Take snapshots and export clips",
|
|
174
|
-
risky: false,
|
|
175
|
-
},
|
|
176
|
-
"media.control": {
|
|
177
|
-
apis: ["MediaCapture.capture", "Snapshot.export"],
|
|
178
|
-
requires: ["media.read"],
|
|
179
|
-
label: "Take snapshots and export clips",
|
|
180
|
-
risky: false,
|
|
181
|
-
},
|
|
182
|
-
"ui.osd": {
|
|
183
|
-
apis: ["Osd.show", "Osd.panel"],
|
|
184
|
-
requires: [],
|
|
185
|
-
label: "Show messages on the video",
|
|
186
|
-
risky: false,
|
|
187
|
-
},
|
|
188
|
-
clipboard: {
|
|
189
|
-
apis: [
|
|
190
|
-
"Clipboard.readText",
|
|
191
|
-
"Clipboard.writeText",
|
|
192
|
-
"Clipboard.writeImage",
|
|
193
|
-
],
|
|
194
|
-
requires: [],
|
|
195
|
-
label: "Read and write the clipboard",
|
|
196
|
-
risky: true,
|
|
197
|
-
},
|
|
198
|
-
"window.control": {
|
|
199
|
-
apis: [
|
|
200
|
-
"MainWindow.setVideoArea",
|
|
201
|
-
"MainWindow.setAlwaysOnTop",
|
|
202
|
-
"MainWindow.setCursorAutoHide",
|
|
203
|
-
"MainWindow.setPictureInPicture",
|
|
204
|
-
],
|
|
205
|
-
requires: [],
|
|
206
|
-
label: "Adjust the main window (size, on top, picture-in-picture)",
|
|
207
|
-
risky: false,
|
|
208
|
-
},
|
|
209
|
-
"bindings.manage": {
|
|
210
|
-
apis: [
|
|
211
|
-
"Bindings.list",
|
|
212
|
-
"Bindings.set",
|
|
213
|
-
"Bindings.reset",
|
|
214
|
-
"Bindings.onChange",
|
|
215
|
-
],
|
|
216
|
-
requires: [],
|
|
217
|
-
label: "Change all shortcuts",
|
|
218
|
-
risky: true,
|
|
219
|
-
},
|
|
220
|
-
"timeline.marks": {
|
|
221
|
-
apis: ["Timeline.layer"],
|
|
222
|
-
requires: [],
|
|
223
|
-
label: "Show marks on the progress bar",
|
|
224
|
-
risky: false,
|
|
225
|
-
},
|
|
226
|
-
"ui.drop": {
|
|
227
|
-
apis: ["Drops.provide", "Panel.onDrop"],
|
|
228
|
-
requires: [],
|
|
229
|
-
label: "Receive files dropped on the player",
|
|
230
|
-
risky: false,
|
|
231
|
-
},
|
|
232
|
-
"ui.settings": {
|
|
233
|
-
apis: ["Settings.page", "Settings.show"],
|
|
234
|
-
requires: [],
|
|
235
|
-
label: "Add pages to the Options window",
|
|
236
|
-
risky: false,
|
|
237
|
-
},
|
|
238
|
-
"ui.theme": {
|
|
239
|
-
apis: ["Appearance.set", "Appearance.setLanguage"],
|
|
240
|
-
requires: [],
|
|
241
|
-
label: "Change the player's palette",
|
|
242
|
-
risky: false,
|
|
243
|
-
},
|
|
244
|
-
secrets: {
|
|
245
|
-
apis: ["Secrets.get", "Secrets.set", "Secrets.delete"],
|
|
246
|
-
requires: [],
|
|
247
|
-
label: "Store sign-in details in the system keychain",
|
|
248
|
-
risky: true,
|
|
249
|
-
},
|
|
250
|
-
});
|
|
28
|
+
export const CAPABILITY_INFO = Object.freeze(Object.fromEntries(Object.entries(capabilityInfo()).map(([name, info]) => [
|
|
29
|
+
name,
|
|
30
|
+
Object.freeze({
|
|
31
|
+
apis: Object.freeze(info.apis),
|
|
32
|
+
requires: Object.freeze(info.requires),
|
|
33
|
+
label: info.label,
|
|
34
|
+
risky: info.risky,
|
|
35
|
+
}),
|
|
36
|
+
])));
|
|
251
37
|
/**
|
|
252
38
|
* Manifest capabilities the player knows (`vestra/1`): the keys of
|
|
253
39
|
* {@link CAPABILITY_INFO}, in the player's order. The same list as the
|
|
254
|
-
* player's `host/
|
|
40
|
+
* player's `host/capabilities.json`; the player's tests check that they agree.
|
|
255
41
|
*/
|
|
256
42
|
export const CAPABILITIES = Object.freeze(Object.keys(CAPABILITY_INFO));
|
|
257
43
|
/** The plugin API version this SDK implements (`manifest.apiVersion`). */
|
|
@@ -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
|
+
}>;
|