@elgato/cli 0.3.0 → 0.3.2
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/bin/streamdeck.mjs +3 -3
- package/dist/index.d.ts +328 -0
- package/dist/index.js +1046 -0
- package/package.json +34 -25
- package/template/.vscode/settings.json +15 -12
- package/template/com.elgato.template.sdPlugin/manifest.json.ejs +1 -0
- package/template/com.elgato.template.sdPlugin/ui/increment-counter.html +19 -0
- package/template/src/actions/increment-counter.ts.ejs +9 -7
package/dist/index.js
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
/**!
|
|
2
|
+
* @author Elgato
|
|
3
|
+
* @module elgato/streamdeck
|
|
4
|
+
* @license MIT
|
|
5
|
+
* @copyright Copyright (c) Corsair Memory Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import chalk6 from "chalk";
|
|
10
|
+
|
|
11
|
+
// src/validation/entry.ts
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { EOL } from "os";
|
|
14
|
+
var ValidationEntry = class {
|
|
15
|
+
/**
|
|
16
|
+
* Initializes a new instance of the {@link ValidationEntry} class.
|
|
17
|
+
* @param level Severity level of the entry.
|
|
18
|
+
* @param message Validation message.
|
|
19
|
+
* @param details Supporting optional details.
|
|
20
|
+
*/
|
|
21
|
+
constructor(level, message, details) {
|
|
22
|
+
this.level = level;
|
|
23
|
+
this.message = message;
|
|
24
|
+
this.details = details;
|
|
25
|
+
if (message.endsWith(".")) {
|
|
26
|
+
this.message = message.slice(0, -1);
|
|
27
|
+
}
|
|
28
|
+
if (this.details?.location?.column || this.details?.location?.line) {
|
|
29
|
+
this.location = `${this.details.location.line}`;
|
|
30
|
+
if (this.details.location.column) {
|
|
31
|
+
this.location += `:${this.details.location.column}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (this.details?.location?.key) {
|
|
35
|
+
this.message = `${chalk.cyan(this.details.location.key)} ${message}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Location of the validation entry, represented as a string in the format {line}:{column}.
|
|
40
|
+
*/
|
|
41
|
+
location = "";
|
|
42
|
+
/**
|
|
43
|
+
* Converts the entry to a summary string.
|
|
44
|
+
* @param padding Optional padding required to align the position of each entry.
|
|
45
|
+
* @returns String that represents the entry.
|
|
46
|
+
*/
|
|
47
|
+
toSummary(padding) {
|
|
48
|
+
const position = padding === void 0 || padding === 0 ? "" : `${this.location.padEnd(padding + 2)}`;
|
|
49
|
+
const level = ValidationLevel[this.level].padEnd(7);
|
|
50
|
+
let message = ` ${chalk.dim(position)}${this.level === 0 /* error */ ? chalk.red(level) : chalk.yellow(level)} ${this.message}`;
|
|
51
|
+
if (this.details?.suggestion) {
|
|
52
|
+
const prefix = chalk.level > 0 ? chalk.hidden(`${position}${level}`) : " ".repeat(position.length + level.length);
|
|
53
|
+
message += `${EOL} ${prefix} ${chalk.dim("\u2514", this.details.suggestion)}`;
|
|
54
|
+
}
|
|
55
|
+
return message;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ValidationLevel = /* @__PURE__ */ ((ValidationLevel2) => {
|
|
59
|
+
ValidationLevel2[ValidationLevel2["error"] = 0] = "error";
|
|
60
|
+
ValidationLevel2[ValidationLevel2["warning"] = 1] = "warning";
|
|
61
|
+
return ValidationLevel2;
|
|
62
|
+
})(ValidationLevel || {});
|
|
63
|
+
|
|
64
|
+
// src/validation/file-result.ts
|
|
65
|
+
import chalk2 from "chalk";
|
|
66
|
+
|
|
67
|
+
// src/common/ordered-array.ts
|
|
68
|
+
var OrderedArray = class extends Array {
|
|
69
|
+
/**
|
|
70
|
+
* Delegates responsible for determining the sort order.
|
|
71
|
+
*/
|
|
72
|
+
#compareOn;
|
|
73
|
+
/**
|
|
74
|
+
* Initializes a new instance of the {@link OrderedArray} class.
|
|
75
|
+
* @param compareOn Delegates responsible for determining the sort order.
|
|
76
|
+
*/
|
|
77
|
+
constructor(...compareOn) {
|
|
78
|
+
super();
|
|
79
|
+
this.#compareOn = compareOn;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* "Pushes" the specified {@link value} in a sorted order.
|
|
83
|
+
* @param value Value to push.
|
|
84
|
+
* @returns New length of the array.
|
|
85
|
+
*/
|
|
86
|
+
push(value) {
|
|
87
|
+
super.splice(this.#sortedIndex(value), 0, value);
|
|
88
|
+
return this.length;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compares {@link a} to {@link b} and returns a numerical representation of the comparison.
|
|
92
|
+
* @param a Item A.
|
|
93
|
+
* @param b Item B.
|
|
94
|
+
* @returns `-1` when {@link a} is less than {@link b}, `1` when {@link a} is greater than {@link b}, otherwise `0`
|
|
95
|
+
*/
|
|
96
|
+
#compare(a, b) {
|
|
97
|
+
for (const compareOn of this.#compareOn) {
|
|
98
|
+
const x = compareOn(a);
|
|
99
|
+
const y = compareOn(b);
|
|
100
|
+
if (x < y) {
|
|
101
|
+
return -1;
|
|
102
|
+
} else if (x > y) {
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Gets the sorted index of the specified {@link value} relative to this instance.
|
|
110
|
+
* Inspired by {@link https://stackoverflow.com/a/21822316}.
|
|
111
|
+
* @param value The value.
|
|
112
|
+
* @returns Index.
|
|
113
|
+
*/
|
|
114
|
+
#sortedIndex(value) {
|
|
115
|
+
let low = 0;
|
|
116
|
+
let high = this.length;
|
|
117
|
+
while (low < high) {
|
|
118
|
+
const mid = low + high >>> 1;
|
|
119
|
+
const comparison = this.#compare(value, this[mid]);
|
|
120
|
+
if (comparison === 0) {
|
|
121
|
+
return mid;
|
|
122
|
+
} else if (comparison > 0) {
|
|
123
|
+
low = mid + 1;
|
|
124
|
+
} else {
|
|
125
|
+
high = mid;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return low;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/validation/file-result.ts
|
|
133
|
+
var FileValidationResult = class extends OrderedArray {
|
|
134
|
+
/**
|
|
135
|
+
* Initializes a new instance of the {@link FileValidationResult} class.
|
|
136
|
+
* @param path Path that groups the entries together.
|
|
137
|
+
*/
|
|
138
|
+
constructor(path) {
|
|
139
|
+
super(
|
|
140
|
+
(x) => x.level,
|
|
141
|
+
(x) => x.details?.location?.line ?? Infinity,
|
|
142
|
+
(x) => x.details?.location?.column ?? Infinity,
|
|
143
|
+
(x) => x.message
|
|
144
|
+
);
|
|
145
|
+
this.path = path;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Tracks the padding required for the location of a validation entry, i.e. the text before the entry level.
|
|
149
|
+
*/
|
|
150
|
+
#padding = 0;
|
|
151
|
+
/**
|
|
152
|
+
* Adds the specified {@link entry} to the collection.
|
|
153
|
+
* @param entry Entry to add.
|
|
154
|
+
* @returns New length of the validation results.
|
|
155
|
+
*/
|
|
156
|
+
push(entry) {
|
|
157
|
+
this.#padding = Math.max(this.#padding, entry.location.length);
|
|
158
|
+
return super.push(entry);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Writes the entry collection to the {@link output}.
|
|
162
|
+
* @param output Output to write to.
|
|
163
|
+
*/
|
|
164
|
+
writeTo(output) {
|
|
165
|
+
if (this.length === 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
output.log(chalk2.underline(this.path));
|
|
169
|
+
if (chalk2.level > 0) {
|
|
170
|
+
output.log(chalk2.hidden(this.path));
|
|
171
|
+
} else {
|
|
172
|
+
output.log();
|
|
173
|
+
}
|
|
174
|
+
this.forEach((entry) => output.log(entry.toSummary(this.#padding)));
|
|
175
|
+
output.log();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/validation/result.ts
|
|
180
|
+
var ValidationResult = class extends Array {
|
|
181
|
+
/**
|
|
182
|
+
* Private backing field for {@link Result.errorCount}.
|
|
183
|
+
*/
|
|
184
|
+
errorCount = 0;
|
|
185
|
+
/**
|
|
186
|
+
* Private backing field for {@link Result.warningCount}.
|
|
187
|
+
*/
|
|
188
|
+
warningCount = 0;
|
|
189
|
+
/**
|
|
190
|
+
* Adds a new validation entry to the result.
|
|
191
|
+
* @param path Directory or file path the entry is associated with.
|
|
192
|
+
* @param entry Validation entry.
|
|
193
|
+
*/
|
|
194
|
+
add(path, entry) {
|
|
195
|
+
if (entry.level === 0 /* error */) {
|
|
196
|
+
this.errorCount++;
|
|
197
|
+
} else {
|
|
198
|
+
this.warningCount++;
|
|
199
|
+
}
|
|
200
|
+
let fileResult = this.find((c) => c.path === path);
|
|
201
|
+
if (fileResult === void 0) {
|
|
202
|
+
fileResult = new FileValidationResult(path);
|
|
203
|
+
this.push(fileResult);
|
|
204
|
+
}
|
|
205
|
+
fileResult.push(entry);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Determines whether the result contains errors.
|
|
209
|
+
* @returns `true` when the result has errors.
|
|
210
|
+
*/
|
|
211
|
+
hasErrors() {
|
|
212
|
+
return this.errorCount > 0;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Determines whether the result contains warnings.
|
|
216
|
+
* @returns `true` when the result has warnings.
|
|
217
|
+
*/
|
|
218
|
+
hasWarnings() {
|
|
219
|
+
return this.warningCount > 0;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Writes the results to the specified {@link output}.
|
|
223
|
+
* @param output Output to write to.
|
|
224
|
+
*/
|
|
225
|
+
writeTo(output) {
|
|
226
|
+
if (this.length > 0) {
|
|
227
|
+
output.log();
|
|
228
|
+
this.forEach((collection) => collection.writeTo(output));
|
|
229
|
+
}
|
|
230
|
+
if (this.hasErrors() && this.hasWarnings()) {
|
|
231
|
+
output.error(
|
|
232
|
+
`${pluralize("problem", this.errorCount + this.warningCount)} (${pluralize("error", this.errorCount)}, ${pluralize("warning", this.warningCount)})`
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (this.hasErrors()) {
|
|
237
|
+
output.error(`Failed with ${pluralize("error", this.errorCount)}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.hasWarnings()) {
|
|
241
|
+
output.warn(pluralize("warning", this.warningCount));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
output.success("Validation successful");
|
|
245
|
+
function pluralize(noun, count) {
|
|
246
|
+
return `${count} ${count === 1 ? noun : `${noun}s`}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/validation/rule.ts
|
|
252
|
+
var rule = (fn) => fn;
|
|
253
|
+
|
|
254
|
+
// src/validation/validator.ts
|
|
255
|
+
async function validate(path, context, rules) {
|
|
256
|
+
const result = new ValidationResult();
|
|
257
|
+
const validationContext = new ValidationContext(path, result);
|
|
258
|
+
for (const rule2 of rules) {
|
|
259
|
+
await rule2.call(validationContext, context);
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
var ValidationContext = class {
|
|
264
|
+
/**
|
|
265
|
+
* Initializes a new instance of the {@link ValidationContext} class.
|
|
266
|
+
* @param path Path to the item being validated.
|
|
267
|
+
* @param result Validation results.
|
|
268
|
+
*/
|
|
269
|
+
constructor(path, result) {
|
|
270
|
+
this.path = path;
|
|
271
|
+
this.result = result;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Adds a validation error.
|
|
275
|
+
* @param path File or directory path the entry is associated with.
|
|
276
|
+
* @param message Validation message.
|
|
277
|
+
* @param details Optional details.
|
|
278
|
+
* @returns This instance for chaining.
|
|
279
|
+
*/
|
|
280
|
+
addError(path, message, details) {
|
|
281
|
+
this.result.add(path, new ValidationEntry(0 /* error */, message, details));
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Adds a validation warning.
|
|
286
|
+
* @param path File or directory path the entry is associated with.
|
|
287
|
+
* @param message Validation message.
|
|
288
|
+
* @param details Optional details.
|
|
289
|
+
* @returns This instance for chaining.
|
|
290
|
+
*/
|
|
291
|
+
addWarning(path, message, details) {
|
|
292
|
+
this.result.add(path, new ValidationEntry(1 /* warning */, message, details));
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/validation/plugin/plugin.ts
|
|
298
|
+
import { basename, dirname, join, resolve } from "path";
|
|
299
|
+
|
|
300
|
+
// src/json/file-context.ts
|
|
301
|
+
import { existsSync, readFileSync } from "fs";
|
|
302
|
+
|
|
303
|
+
// src/json/map.ts
|
|
304
|
+
var JsonObjectMap = class {
|
|
305
|
+
/**
|
|
306
|
+
* Parsed data.
|
|
307
|
+
*/
|
|
308
|
+
value = {};
|
|
309
|
+
/**
|
|
310
|
+
* Collection of AST nodes indexed by their instance path (pointer).
|
|
311
|
+
*/
|
|
312
|
+
nodes = /* @__PURE__ */ new Map();
|
|
313
|
+
/**
|
|
314
|
+
* Initializes a new instance of the {@link JsonObjectMap} class.
|
|
315
|
+
* @param node Source that contains the data.
|
|
316
|
+
* @param errors JSON schema errors; used to determine invalid types based on the instance path of an error.
|
|
317
|
+
*/
|
|
318
|
+
constructor(node, errors) {
|
|
319
|
+
if (node?.type === "Object") {
|
|
320
|
+
this.value = this.aggregate(node, "", errors);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Finds the {@link NodeRef} from its {@link instancePath}.
|
|
325
|
+
* @param instancePath Instance path.
|
|
326
|
+
* @returns The node associated with the {@link instancePath}.
|
|
327
|
+
*/
|
|
328
|
+
find(instancePath) {
|
|
329
|
+
return this.nodes.get(instancePath);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Aggregates the {@param node} to an object containing the property values and their paths.
|
|
333
|
+
* @param node Node to aggregate.
|
|
334
|
+
* @param pointer Pointer to the {@param node}.
|
|
335
|
+
* @param errors Errors associated with the JSON used to parse the {@param node}.
|
|
336
|
+
* @returns Aggregated object.
|
|
337
|
+
*/
|
|
338
|
+
aggregate(node, pointer, errors) {
|
|
339
|
+
const nodeRef = {
|
|
340
|
+
location: {
|
|
341
|
+
...node.loc?.start,
|
|
342
|
+
instancePath: pointer,
|
|
343
|
+
key: getPath(pointer)
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
this.nodes.set(pointer, nodeRef);
|
|
347
|
+
if (errors?.find((e) => e.instancePath === pointer && e.keyword === "type")) {
|
|
348
|
+
nodeRef.node = new JsonValueNode(void 0, nodeRef.location);
|
|
349
|
+
return nodeRef.node;
|
|
350
|
+
}
|
|
351
|
+
if (node.type === "Object") {
|
|
352
|
+
return node.members.reduce(
|
|
353
|
+
(obj, member) => {
|
|
354
|
+
obj[member.name.value] = this.aggregate(member.value, `${pointer}/${member.name.value}`, errors);
|
|
355
|
+
return obj;
|
|
356
|
+
},
|
|
357
|
+
{}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (node.type === "Array") {
|
|
361
|
+
return node.elements.map((item, i) => this.aggregate(item.value, `${pointer}/${i}`, errors));
|
|
362
|
+
}
|
|
363
|
+
if (node.type === "Boolean" || node.type === "Number" || node.type === "String") {
|
|
364
|
+
nodeRef.node = new JsonValueNode(node.value, nodeRef.location);
|
|
365
|
+
return nodeRef.node;
|
|
366
|
+
}
|
|
367
|
+
if (node.type === "Null") {
|
|
368
|
+
nodeRef.node = new JsonValueNode(null, nodeRef.location);
|
|
369
|
+
return nodeRef.node;
|
|
370
|
+
}
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Encountered unhandled node type '${node.type}' when mapping abstract-syntax tree node to JSON object`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var JsonValueNode = class {
|
|
377
|
+
/**
|
|
378
|
+
* Initializes a new instance of the {@link JsonValueNode} class.
|
|
379
|
+
* @param value Parsed value.
|
|
380
|
+
* @param location Location of the element within the JSON it was parsed from.
|
|
381
|
+
*/
|
|
382
|
+
constructor(value, location) {
|
|
383
|
+
this.value = value;
|
|
384
|
+
this.location = location;
|
|
385
|
+
}
|
|
386
|
+
/** @inheritdoc */
|
|
387
|
+
toString() {
|
|
388
|
+
return this.value?.toString();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function getPath(pointer) {
|
|
392
|
+
const path = pointer.split("/").reduce((path2, segment) => {
|
|
393
|
+
if (segment === void 0 || segment === "") {
|
|
394
|
+
return path2;
|
|
395
|
+
}
|
|
396
|
+
if (!isNaN(Number(segment))) {
|
|
397
|
+
return `${path2}[${segment}]`;
|
|
398
|
+
}
|
|
399
|
+
return `${path2}.${segment}`;
|
|
400
|
+
}, "");
|
|
401
|
+
return path.startsWith(".") ? path.slice(1) : path;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/json/file-context.ts
|
|
405
|
+
var JsonFileContext = class {
|
|
406
|
+
/**
|
|
407
|
+
* Initializes a new instance of the {@link JsonFileContext} class.
|
|
408
|
+
* @param path Path to the file, as defined within the plugin; the file may or may not exist.
|
|
409
|
+
* @param schema JSON schema to use when validating the file.
|
|
410
|
+
*/
|
|
411
|
+
constructor(path, schema) {
|
|
412
|
+
this.path = path;
|
|
413
|
+
this.schema = schema;
|
|
414
|
+
if (existsSync(this.path)) {
|
|
415
|
+
const json = readFileSync(this.path, { encoding: "utf-8" });
|
|
416
|
+
({ errors: this.errors, map: this._map } = this.schema.validate(json));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Collection of JSON schema validation errors.
|
|
421
|
+
*/
|
|
422
|
+
errors = [];
|
|
423
|
+
/**
|
|
424
|
+
* Map of the parsed JSON data.
|
|
425
|
+
*/
|
|
426
|
+
_map = new JsonObjectMap();
|
|
427
|
+
/**
|
|
428
|
+
* Parsed data with all valid value types set, including the location of which the value was parsed within the JSON.
|
|
429
|
+
* @returns Parsed data.
|
|
430
|
+
*/
|
|
431
|
+
get value() {
|
|
432
|
+
return this._map.value;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Finds the node reference for the specified {@link instancePath}.
|
|
436
|
+
* @param instancePath Instance path.
|
|
437
|
+
* @returns The node associated with the {@link instancePath}.
|
|
438
|
+
*/
|
|
439
|
+
find(instancePath) {
|
|
440
|
+
return this._map.find(instancePath);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// src/json/schema.ts
|
|
445
|
+
import { keywordDefinitions } from "@elgato/schemas";
|
|
446
|
+
import { parse } from "@humanwhocodes/momoa";
|
|
447
|
+
import Ajv from "ajv";
|
|
448
|
+
import _ from "lodash";
|
|
449
|
+
|
|
450
|
+
// src/common/stdout.ts
|
|
451
|
+
import chalk3 from "chalk";
|
|
452
|
+
import isInteractive from "is-interactive";
|
|
453
|
+
import logSymbols from "log-symbols";
|
|
454
|
+
function colorize(value) {
|
|
455
|
+
if (typeof value === "string") {
|
|
456
|
+
return chalk3.green(`'${value}'`);
|
|
457
|
+
}
|
|
458
|
+
return chalk3.yellow(value);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/common/utils.ts
|
|
462
|
+
function aggregate(items, conjunction, transform) {
|
|
463
|
+
const fn = transform || ((value) => value);
|
|
464
|
+
return items.reduce((prev, current, index) => {
|
|
465
|
+
const value = fn(current);
|
|
466
|
+
if (index === 0) {
|
|
467
|
+
return value;
|
|
468
|
+
} else if (index === items.length - 1 && index > 0) {
|
|
469
|
+
return `${prev}, ${conjunction} ${value}`;
|
|
470
|
+
} else {
|
|
471
|
+
return `${prev}, ${value}`;
|
|
472
|
+
}
|
|
473
|
+
}, "");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/json/schema.ts
|
|
477
|
+
var JsonSchema = class {
|
|
478
|
+
/**
|
|
479
|
+
* Private backing field for {@link filePathsKeywords}.
|
|
480
|
+
*/
|
|
481
|
+
_filePathsKeywords = /* @__PURE__ */ new Map();
|
|
482
|
+
/**
|
|
483
|
+
* Private backing field for {@link imageDimensionKeywords}.
|
|
484
|
+
*/
|
|
485
|
+
_imageDimensionKeywords = /* @__PURE__ */ new Map();
|
|
486
|
+
/**
|
|
487
|
+
* Internal validator.
|
|
488
|
+
*/
|
|
489
|
+
_validate;
|
|
490
|
+
/**
|
|
491
|
+
* Collection of custom error messages, indexed by their JSON instance path, defined with the JSON schema using `@errorMessage`.
|
|
492
|
+
*/
|
|
493
|
+
errorMessages = /* @__PURE__ */ new Map();
|
|
494
|
+
/**
|
|
495
|
+
* Initializes a new instance of the {@link JsonSchema} class.
|
|
496
|
+
* @param schema Schema that defines the JSON structure.
|
|
497
|
+
*/
|
|
498
|
+
constructor(schema) {
|
|
499
|
+
const ajv = new Ajv({
|
|
500
|
+
allErrors: true,
|
|
501
|
+
messages: false,
|
|
502
|
+
strict: false
|
|
503
|
+
});
|
|
504
|
+
ajv.addKeyword(keywordDefinitions.markdownDescription);
|
|
505
|
+
ajv.addKeyword(captureKeyword(keywordDefinitions.errorMessage, this.errorMessages));
|
|
506
|
+
ajv.addKeyword(captureKeyword(keywordDefinitions.imageDimensions, this._imageDimensionKeywords));
|
|
507
|
+
ajv.addKeyword(captureKeyword(keywordDefinitions.filePath, this._filePathsKeywords));
|
|
508
|
+
this._validate = ajv.compile(schema);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Collection of {@link FilePathOptions}, indexed by their JSON instance path, defined with the JSON schema using `@filePath`.
|
|
512
|
+
* @returns The collection of {@link FilePathOptions}.
|
|
513
|
+
*/
|
|
514
|
+
get filePathsKeywords() {
|
|
515
|
+
return this._filePathsKeywords;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Collection of {@link ImageDimensions}, indexed by their JSON instance path, defined with the JSON schema using `@imageDimensions`.
|
|
519
|
+
* @returns The collection of {@link FilePathOptions}.
|
|
520
|
+
*/
|
|
521
|
+
get imageDimensionKeywords() {
|
|
522
|
+
return this._imageDimensionKeywords;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Validates the {@param json}.
|
|
526
|
+
* @param json JSON string to parse.
|
|
527
|
+
* @returns Data that could be successfully parsed from the {@param json}, and a collection of errors.
|
|
528
|
+
*/
|
|
529
|
+
validate(json) {
|
|
530
|
+
this._filePathsKeywords.clear();
|
|
531
|
+
this._imageDimensionKeywords.clear();
|
|
532
|
+
let data;
|
|
533
|
+
try {
|
|
534
|
+
data = JSON.parse(json);
|
|
535
|
+
} catch {
|
|
536
|
+
return {
|
|
537
|
+
map: new JsonObjectMap(),
|
|
538
|
+
errors: [
|
|
539
|
+
{
|
|
540
|
+
source: {
|
|
541
|
+
keyword: "false schema",
|
|
542
|
+
instancePath: "",
|
|
543
|
+
schemaPath: "",
|
|
544
|
+
params: {}
|
|
545
|
+
},
|
|
546
|
+
message: "Contents must be a valid JSON string",
|
|
547
|
+
location: { instancePath: "/", key: void 0 }
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
this._validate(data);
|
|
553
|
+
const ast = parse(json, { mode: "json", ranges: false, tokens: false });
|
|
554
|
+
const map = new JsonObjectMap(ast.body, this._validate.errors);
|
|
555
|
+
return {
|
|
556
|
+
map,
|
|
557
|
+
errors: this.filter(this._validate.errors).map((source) => ({
|
|
558
|
+
location: map.find(source.instancePath)?.location,
|
|
559
|
+
message: this.getMessage(source),
|
|
560
|
+
source
|
|
561
|
+
})) ?? []
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Filters the errors, removing ignored keywords and duplicates.
|
|
566
|
+
* @param errors Errors to filter.
|
|
567
|
+
* @returns Filtered errors.
|
|
568
|
+
*/
|
|
569
|
+
filter(errors) {
|
|
570
|
+
if (errors === void 0 || errors === null) {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
const ignoredKeywords = ["allOf", "anyOf", "if"];
|
|
574
|
+
return _.uniqWith(
|
|
575
|
+
errors.filter(({ keyword }) => !ignoredKeywords.includes(keyword)),
|
|
576
|
+
(a, b) => a.instancePath === b.instancePath && a.keyword === b.keyword && _.isEqual(a.params, b.params)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Parses the error message from the specified {@link ErrorObject}.
|
|
581
|
+
* @param error JSON schema error.
|
|
582
|
+
* @returns The error message.
|
|
583
|
+
*/
|
|
584
|
+
getMessage(error) {
|
|
585
|
+
const { keyword, message, params, instancePath } = error;
|
|
586
|
+
if (keyword === "additionalProperties") {
|
|
587
|
+
return params.additionalProperty !== void 0 ? `must not contain property: ${params.additionalProperty}` : "must not contain additional properties";
|
|
588
|
+
}
|
|
589
|
+
if (keyword === "enum") {
|
|
590
|
+
const values = aggregate(params.allowedValues, "or", colorize);
|
|
591
|
+
return values !== void 0 ? `must be ${values}` : message || `failed validation for keyword: ${keyword}`;
|
|
592
|
+
}
|
|
593
|
+
if (keyword === "pattern") {
|
|
594
|
+
const errorMessage = this.errorMessages.get(instancePath);
|
|
595
|
+
if (errorMessage?.startsWith("String")) {
|
|
596
|
+
return errorMessage.substring(7);
|
|
597
|
+
}
|
|
598
|
+
return errorMessage || `must match pattern ${params.pattern}`;
|
|
599
|
+
}
|
|
600
|
+
if (keyword === "minimum" || keyword === "maximum") {
|
|
601
|
+
return `must be ${getComparison(params.comparison)} ${params.limit}`;
|
|
602
|
+
}
|
|
603
|
+
if (keyword === "minItems") {
|
|
604
|
+
return `must contain at least ${params.limit} item${params.limit === 1 ? "" : "s"}`;
|
|
605
|
+
}
|
|
606
|
+
if (keyword === "maxItems") {
|
|
607
|
+
return `must not contain more than ${params.limit} item${params.limit === 1 ? "" : "s"}`;
|
|
608
|
+
}
|
|
609
|
+
if (keyword === "required") {
|
|
610
|
+
return `must contain property: ${params.missingProperty}`;
|
|
611
|
+
}
|
|
612
|
+
if (keyword === "type") {
|
|
613
|
+
return `must be a${params.type === "object" ? "n" : ""} ${params.type}`;
|
|
614
|
+
}
|
|
615
|
+
if (keyword === "uniqueItems") {
|
|
616
|
+
return "must not contain duplicate items";
|
|
617
|
+
}
|
|
618
|
+
return message || `failed validation for keyword: ${keyword}`;
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
function captureKeyword(def, map) {
|
|
622
|
+
const { keyword, schemaType } = def;
|
|
623
|
+
return {
|
|
624
|
+
keyword,
|
|
625
|
+
schemaType,
|
|
626
|
+
validate: (schema, data, parentSchema, dataCtx) => {
|
|
627
|
+
if (dataCtx?.instancePath !== void 0) {
|
|
628
|
+
map.set(dataCtx.instancePath, schema);
|
|
629
|
+
}
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function getComparison(comparison) {
|
|
635
|
+
switch (comparison) {
|
|
636
|
+
case "<":
|
|
637
|
+
return "less than";
|
|
638
|
+
case "<=":
|
|
639
|
+
return "less than or equal to";
|
|
640
|
+
case ">":
|
|
641
|
+
return "greater than";
|
|
642
|
+
case ">=":
|
|
643
|
+
return "greater than or equal to";
|
|
644
|
+
default:
|
|
645
|
+
throw new TypeError(`Expected comparison when validating JSON: ${comparison}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/stream-deck.ts
|
|
650
|
+
import find from "find-process";
|
|
651
|
+
function isPredefinedLayoutLike(value) {
|
|
652
|
+
return value.startsWith("$") === true && !value.endsWith(".json");
|
|
653
|
+
}
|
|
654
|
+
function isValidPluginId(uuid) {
|
|
655
|
+
if (uuid === void 0 || uuid === null) {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
return /^([a-z0-9-]+)(\.[a-z0-9-]+)+$/.test(uuid);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/validation/plugin/plugin.ts
|
|
662
|
+
var directorySuffix = ".sdPlugin";
|
|
663
|
+
async function createContext(path) {
|
|
664
|
+
const id = basename(path).replace(/\.sdPlugin$/, "");
|
|
665
|
+
const { manifest, layout } = await import("@elgato/schemas/streamdeck/plugins/json");
|
|
666
|
+
return {
|
|
667
|
+
hasValidId: isValidPluginId(id),
|
|
668
|
+
manifest: new ManifestJsonFileContext(join(path, "manifest.json"), manifest, layout),
|
|
669
|
+
id
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
var ManifestJsonFileContext = class extends JsonFileContext {
|
|
673
|
+
/**
|
|
674
|
+
* Layout files referenced by the manifest.
|
|
675
|
+
*/
|
|
676
|
+
layoutFiles = [];
|
|
677
|
+
/**
|
|
678
|
+
* Initializes a new instance of the {@link ManifestJsonFileContext} class.
|
|
679
|
+
* @param path Path to the manifest file.
|
|
680
|
+
* @param manifestSchema JSON schema that defines the manifest.
|
|
681
|
+
* @param layoutSchema JSON schema that defines a layout.
|
|
682
|
+
*/
|
|
683
|
+
constructor(path, manifestSchema, layoutSchema) {
|
|
684
|
+
super(path, new JsonSchema(manifestSchema));
|
|
685
|
+
const compiledLayoutSchema = new JsonSchema(layoutSchema);
|
|
686
|
+
this.value.Actions?.forEach((action) => {
|
|
687
|
+
if (action.Encoder?.layout !== void 0 && !isPredefinedLayoutLike(action.Encoder?.layout.value)) {
|
|
688
|
+
const filePath = resolve(dirname(path), action.Encoder.layout.value);
|
|
689
|
+
this.layoutFiles.push({
|
|
690
|
+
location: action.Encoder.layout.location,
|
|
691
|
+
layout: new JsonFileContext(filePath, compiledLayoutSchema)
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/validation/plugin/rules/layout-item-bounds.ts
|
|
699
|
+
import chalk4 from "chalk";
|
|
700
|
+
var layoutItemsAreWithinBoundsAndNoOverlap = rule(function(plugin) {
|
|
701
|
+
plugin.manifest.layoutFiles.forEach(({ layout }) => {
|
|
702
|
+
const items = getItemBounds(layout.value);
|
|
703
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
704
|
+
const {
|
|
705
|
+
node,
|
|
706
|
+
vertices: { x1, x2, y1, y2 }
|
|
707
|
+
} = items[i];
|
|
708
|
+
if (x1 < 0 || x2 > 200 || y1 < 0 || y2 > 100) {
|
|
709
|
+
this.addError(layout.path, "must not be outside of the canvas", {
|
|
710
|
+
...node,
|
|
711
|
+
suggestion: "Width and height, relative to the x and y, must be within the 200x100 px canvas"
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
715
|
+
if (isOverlap(items[i].vertices, items[j].vertices)) {
|
|
716
|
+
this.addError(layout.path, `must not overlap ${chalk4.blue(items[j].node.location.key)}`, items[i].node);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
function getItemBounds(layout) {
|
|
723
|
+
return layout.items?.reduce((valid, { rect, zOrder }) => {
|
|
724
|
+
if (rect?.length === 4) {
|
|
725
|
+
valid.push({
|
|
726
|
+
node: rect[0],
|
|
727
|
+
vertices: {
|
|
728
|
+
x1: rect[0].value,
|
|
729
|
+
x2: rect[0].value + rect[2].value,
|
|
730
|
+
y1: rect[1].value,
|
|
731
|
+
y2: rect[1].value + rect[3].value,
|
|
732
|
+
z: zOrder?.value ?? 0
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return valid;
|
|
737
|
+
}, []) || [];
|
|
738
|
+
}
|
|
739
|
+
function isOverlap(a, b) {
|
|
740
|
+
if (a.z !== b.z) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
return !(b.x2 <= a.x1 || b.x1 >= a.x2 || b.y2 <= a.y1 || b.y1 >= a.y2);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/validation/plugin/rules/layout-item-keys.ts
|
|
747
|
+
var layoutItemKeysAreUnique = rule(function(plugin) {
|
|
748
|
+
plugin.manifest.layoutFiles.forEach(({ layout }) => {
|
|
749
|
+
const keys = /* @__PURE__ */ new Set();
|
|
750
|
+
layout.value.items?.forEach(({ key }) => {
|
|
751
|
+
if (key?.value === void 0) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (keys.has(key.value)) {
|
|
755
|
+
this.addError(layout.path, "must be unique", key);
|
|
756
|
+
} else {
|
|
757
|
+
keys.add(key.value);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// src/validation/plugin/rules/layout-schema.ts
|
|
764
|
+
import { existsSync as existsSync2 } from "fs";
|
|
765
|
+
var layoutsExistAndSchemasAreValid = rule(function(plugin) {
|
|
766
|
+
plugin.manifest.layoutFiles.forEach(({ layout, location }) => {
|
|
767
|
+
if (!existsSync2(layout.path)) {
|
|
768
|
+
this.addError(plugin.manifest.path, "layout not found", { location });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
plugin.manifest.layoutFiles.forEach(({ layout }) => {
|
|
772
|
+
layout.errors.forEach(({ message, location, source }) => {
|
|
773
|
+
this.addError(layout.path, transformMessage(message, source), { location });
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
function transformMessage(message, source) {
|
|
778
|
+
if (source.keyword !== "minimum" && source.keyword !== "maximum") {
|
|
779
|
+
return message;
|
|
780
|
+
}
|
|
781
|
+
const match = source.instancePath.match(/\/items\/\d+\/rect\/([0-3])$/);
|
|
782
|
+
if (match === null) {
|
|
783
|
+
return message;
|
|
784
|
+
}
|
|
785
|
+
const [, index] = match;
|
|
786
|
+
return `${["x", "y", "width", "height"][index]} ${message}`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/validation/plugin/rules/manifest-category.ts
|
|
790
|
+
var categoryMatchesName = rule(function(plugin) {
|
|
791
|
+
const {
|
|
792
|
+
manifest: {
|
|
793
|
+
value: { Category: category, Name: name }
|
|
794
|
+
}
|
|
795
|
+
} = plugin;
|
|
796
|
+
if (name?.value !== void 0 && category?.value !== name?.value) {
|
|
797
|
+
const val = category?.value === void 0 ? void 0 : `'${category}'`;
|
|
798
|
+
this.addWarning(plugin.manifest.path, `should match plugin name`, {
|
|
799
|
+
location: {
|
|
800
|
+
key: "Category"
|
|
801
|
+
},
|
|
802
|
+
...category,
|
|
803
|
+
suggestion: `Expected '${name}', but was ${val}`
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// src/validation/plugin/rules/manifest-files-exist.ts
|
|
809
|
+
import { existsSync as existsSync4 } from "fs";
|
|
810
|
+
import { basename as basename3, extname, join as join3, resolve as resolve3 } from "path";
|
|
811
|
+
|
|
812
|
+
// src/system/fs.ts
|
|
813
|
+
import ignore from "ignore";
|
|
814
|
+
import { cpSync, createReadStream, existsSync as existsSync3, lstatSync, mkdirSync, readlinkSync, rmSync } from "fs";
|
|
815
|
+
import { basename as basename2, join as join2, resolve as resolve2 } from "path";
|
|
816
|
+
import { createInterface } from "readline";
|
|
817
|
+
var streamDeckIgnoreFilename = ".sdignore";
|
|
818
|
+
var defaultIgnorePatterns = [streamDeckIgnoreFilename, ".git", "/.env*", "*.log", "*.js.map"];
|
|
819
|
+
async function getIgnores(path, defaultPatterns = defaultIgnorePatterns) {
|
|
820
|
+
const i = ignore().add(defaultPatterns);
|
|
821
|
+
const file = join2(path, streamDeckIgnoreFilename);
|
|
822
|
+
if (existsSync3(file)) {
|
|
823
|
+
const fileStream = createReadStream(file);
|
|
824
|
+
try {
|
|
825
|
+
const rl = createInterface({
|
|
826
|
+
input: fileStream,
|
|
827
|
+
crlfDelay: Infinity
|
|
828
|
+
});
|
|
829
|
+
for await (const line of rl) {
|
|
830
|
+
i.add(line);
|
|
831
|
+
}
|
|
832
|
+
} finally {
|
|
833
|
+
fileStream.close();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return (p) => i.ignores(p);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/validation/plugin/rules/manifest-files-exist.ts
|
|
840
|
+
var manifestFilesExist = rule(async function(plugin) {
|
|
841
|
+
const missingHighRes = /* @__PURE__ */ new Set();
|
|
842
|
+
const ignores = await getIgnores(this.path);
|
|
843
|
+
if (ignores(basename3(plugin.manifest.path))) {
|
|
844
|
+
this.addError(plugin.manifest.path, "Manifest file must not be ignored", {
|
|
845
|
+
suggestion: `Review ${streamDeckIgnoreFilename} file`
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
const filePaths = new Map(plugin.manifest.schema.filePathsKeywords);
|
|
849
|
+
plugin.manifest.value.Actions?.forEach((action) => {
|
|
850
|
+
if (action.Encoder?.layout?.value !== void 0 && isPredefinedLayoutLike(action.Encoder?.layout?.value)) {
|
|
851
|
+
filePaths.delete(action.Encoder.layout.location.instancePath);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
filePaths.forEach((opts, instancePath) => {
|
|
855
|
+
const nodeRef = plugin.manifest.find(instancePath);
|
|
856
|
+
if (nodeRef?.node === void 0) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const { node } = nodeRef;
|
|
860
|
+
if (typeof node.value !== "string" || plugin.manifest.errors.find((e) => e.location?.instancePath === instancePath)) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const possiblePaths = typeof opts === "object" && !opts.includeExtension ? opts.extensions.map((ext) => `${node.value}${ext}`) : [node.value];
|
|
864
|
+
let resolvedPath = void 0;
|
|
865
|
+
for (const possiblePath of possiblePaths) {
|
|
866
|
+
const path = resolve3(this.path, possiblePath);
|
|
867
|
+
if (existsSync4(path)) {
|
|
868
|
+
if (resolvedPath !== void 0) {
|
|
869
|
+
this.addWarning(
|
|
870
|
+
plugin.manifest.path,
|
|
871
|
+
`multiple files named ${colorize(node.value)} found, using ${colorize(resolvedPath)}`,
|
|
872
|
+
node
|
|
873
|
+
);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
resolvedPath = possiblePath;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (resolvedPath === void 0) {
|
|
880
|
+
this.addError(plugin.manifest.path, `file not found, ${colorize(node.value)}`, {
|
|
881
|
+
...node,
|
|
882
|
+
suggestion: typeof opts === "object" ? `File must be ${aggregate(opts.extensions, "or")}` : void 0
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (ignores(resolvedPath)) {
|
|
887
|
+
this.addError(plugin.manifest.path, `file must not be ignored, ${colorize(resolvedPath)}`, {
|
|
888
|
+
...node,
|
|
889
|
+
suggestion: `Review ${streamDeckIgnoreFilename} file`
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (extname(resolvedPath) === ".png") {
|
|
894
|
+
const fullPath = join3(this.path, resolvedPath);
|
|
895
|
+
if (missingHighRes.has(fullPath)) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (!existsSync4(join3(this.path, `${node.value}@2x.png`))) {
|
|
899
|
+
this.addWarning(fullPath, "should have high-resolution (@2x) variant", {
|
|
900
|
+
location: {
|
|
901
|
+
key: node.location.key
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
missingHighRes.add(fullPath);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/validation/plugin/rules/manifest-schema.ts
|
|
911
|
+
import { existsSync as existsSync5 } from "fs";
|
|
912
|
+
var manifestExistsAndSchemaIsValid = rule(function(plugin) {
|
|
913
|
+
if (!existsSync5(this.path)) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (!existsSync5(plugin.manifest.path)) {
|
|
917
|
+
this.addError(plugin.manifest.path, "Manifest not found");
|
|
918
|
+
}
|
|
919
|
+
plugin.manifest.errors.forEach(({ message, location }) => {
|
|
920
|
+
if (plugin.hasValidId && location?.instancePath === "" && message === "must contain property: UUID") {
|
|
921
|
+
this.addError(plugin.manifest.path, message, {
|
|
922
|
+
location,
|
|
923
|
+
suggestion: `Expected: ${plugin.id}`
|
|
924
|
+
});
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.addError(plugin.manifest.path, message, { location });
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// src/validation/plugin/rules/manifest-urls-exist.ts
|
|
932
|
+
import chalk5 from "chalk";
|
|
933
|
+
var manifestUrlsExist = rule(async function(plugin) {
|
|
934
|
+
const {
|
|
935
|
+
manifest: {
|
|
936
|
+
value: { URL: url }
|
|
937
|
+
}
|
|
938
|
+
} = plugin;
|
|
939
|
+
if (url?.value == void 0) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
let parsedUrl;
|
|
943
|
+
try {
|
|
944
|
+
parsedUrl = new URL(url.value);
|
|
945
|
+
} catch {
|
|
946
|
+
this.addError(plugin.manifest.path, "must be valid URL", {
|
|
947
|
+
...url,
|
|
948
|
+
suggestion: !url.value.toLowerCase().startsWith("http") ? "Protocol must be http or https" : void 0
|
|
949
|
+
});
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
953
|
+
this.addError(plugin.manifest.path, "must have http or https protocol", url);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
const { status } = await fetch(url.value, { method: "HEAD" });
|
|
958
|
+
if (status < 200 || status >= 300) {
|
|
959
|
+
this.addWarning(plugin.manifest.path, `should return success (received ${chalk5.yellow(status)})`, {
|
|
960
|
+
...url,
|
|
961
|
+
suggestion: "Status code should be 2xx"
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
} catch (err) {
|
|
965
|
+
if (err instanceof Error && typeof err.cause === "object" && err.cause && "code" in err.cause && err.cause.code === "ENOTFOUND") {
|
|
966
|
+
this.addError(plugin.manifest.path, "must be resolvable", url);
|
|
967
|
+
} else {
|
|
968
|
+
throw err;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// src/validation/plugin/rules/manifest-uuids.ts
|
|
974
|
+
var manifestUuids = rule(async function(plugin) {
|
|
975
|
+
const { value: manifest } = plugin.manifest;
|
|
976
|
+
if (plugin.hasValidId && manifest.UUID?.value !== void 0 && plugin.id !== manifest.UUID.value) {
|
|
977
|
+
this.addError(plugin.manifest.path, "must match parent directory name", {
|
|
978
|
+
location: manifest.UUID.location,
|
|
979
|
+
suggestion: `Expected: ${plugin.id}`
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
const uuids = /* @__PURE__ */ new Set();
|
|
983
|
+
manifest.Actions?.forEach(({ UUID: uuid }) => {
|
|
984
|
+
if (uuid?.value === void 0) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (uuids.has(uuid.value)) {
|
|
988
|
+
this.addError(plugin.manifest.path, "must be unique", uuid);
|
|
989
|
+
} else {
|
|
990
|
+
uuids.add(uuid.value);
|
|
991
|
+
}
|
|
992
|
+
if (plugin.hasValidId && !uuid.value.startsWith(plugin.id)) {
|
|
993
|
+
this.addWarning(plugin.manifest.path, `should be prefixed with ${colorize(plugin.id)}`, uuid);
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
// src/validation/plugin/rules/path-input.ts
|
|
999
|
+
import { existsSync as existsSync6, lstatSync as lstatSync2 } from "fs";
|
|
1000
|
+
import { basename as basename4 } from "path";
|
|
1001
|
+
var pathIsDirectoryAndUuid = rule(function(plugin) {
|
|
1002
|
+
const name = basename4(this.path);
|
|
1003
|
+
if (!existsSync6(this.path)) {
|
|
1004
|
+
this.addError(this.path, "Directory not found");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!lstatSync2(this.path).isDirectory()) {
|
|
1008
|
+
this.addError(this.path, "Path must be a directory");
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (!name.endsWith(directorySuffix)) {
|
|
1012
|
+
this.addError(this.path, `Name must be suffixed with ${colorize(".sdPlugin")}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (!isValidPluginId(plugin.id)) {
|
|
1015
|
+
this.addError(
|
|
1016
|
+
this.path,
|
|
1017
|
+
"Name must be in reverse DNS format, and must only contain lowercase alphanumeric characters (a-z, 0-9), hyphens (-), and periods (.)",
|
|
1018
|
+
{
|
|
1019
|
+
suggestion: "Example: com.elgato.wave-link"
|
|
1020
|
+
}
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// src/validation/plugin/index.ts
|
|
1026
|
+
async function validatePlugin(path) {
|
|
1027
|
+
const ctx = await createContext(path);
|
|
1028
|
+
return validate(path, ctx, [
|
|
1029
|
+
pathIsDirectoryAndUuid,
|
|
1030
|
+
manifestExistsAndSchemaIsValid,
|
|
1031
|
+
manifestFilesExist,
|
|
1032
|
+
manifestUuids,
|
|
1033
|
+
manifestUrlsExist,
|
|
1034
|
+
categoryMatchesName,
|
|
1035
|
+
layoutsExistAndSchemasAreValid,
|
|
1036
|
+
layoutItemKeysAreUnique,
|
|
1037
|
+
layoutItemsAreWithinBoundsAndNoOverlap
|
|
1038
|
+
]);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/index.ts
|
|
1042
|
+
chalk6.level = 0;
|
|
1043
|
+
export {
|
|
1044
|
+
ValidationLevel,
|
|
1045
|
+
validatePlugin as validateStreamDeckPlugin
|
|
1046
|
+
};
|