@fcannizzaro/streamdeck-react 0.1.13 → 0.1.14
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/dist/action.d.ts +2 -2
- package/dist/action.js +2 -1
- package/dist/bundler-shared.d.ts +21 -0
- package/dist/bundler-shared.js +37 -2
- package/dist/index.d.ts +2 -1
- package/dist/manifest-extract.d.ts +32 -0
- package/dist/manifest-extract.js +141 -0
- package/dist/manifest-gen.d.ts +52 -0
- package/dist/manifest-gen.js +229 -0
- package/dist/manifest-types.d.ts +238 -0
- package/dist/render/render-pool.js +1 -1
- package/dist/rollup.d.ts +36 -9
- package/dist/rollup.js +41 -14
- package/dist/types.d.ts +20 -34
- package/dist/vite.d.ts +23 -7
- package/dist/vite.js +46 -13
- package/package.json +2 -1
- package/dist/manifest-codegen.d.ts +0 -38
- package/dist/manifest-codegen.js +0 -110
package/dist/action.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActionConfig, ActionDefinition } from './types';
|
|
2
2
|
import { JsonObject } from '@elgato/utils';
|
|
3
|
-
export declare function defineAction<S extends JsonObject = JsonObject>(config:
|
|
3
|
+
export declare function defineAction<S extends JsonObject = JsonObject>(config: ActionConfig<S>): ActionDefinition<S>;
|
package/dist/action.js
CHANGED
|
@@ -7,7 +7,8 @@ function defineAction(config) {
|
|
|
7
7
|
touchStrip: config.touchStrip,
|
|
8
8
|
dialLayout: config.dialLayout,
|
|
9
9
|
wrapper: config.wrapper,
|
|
10
|
-
defaultSettings: config.defaultSettings ?? {}
|
|
10
|
+
defaultSettings: config.defaultSettings ?? {},
|
|
11
|
+
info: config.info
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
//#endregion
|
package/dist/bundler-shared.d.ts
CHANGED
|
@@ -53,3 +53,24 @@ export declare function expandTargets(targets: StreamDeckTarget[]): ResolvedTarg
|
|
|
53
53
|
* In production: missing bindings throw an error (the plugin won't work).
|
|
54
54
|
*/
|
|
55
55
|
export declare function copyNativeBindings(outDir: string, isDevelopment: boolean, options: StreamDeckTargetOptions, warn: (message: string) => void): void;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the .sdPlugin directory root from the bundler output directory.
|
|
58
|
+
*
|
|
59
|
+
* Strategy:
|
|
60
|
+
* 1. Walk up from outDir looking for a parent named `*.sdPlugin`
|
|
61
|
+
* 2. If not found, assume outDir's parent is the .sdPlugin root
|
|
62
|
+
* (handles `<uuid>.sdPlugin/bin/` → `<uuid>.sdPlugin/`)
|
|
63
|
+
*
|
|
64
|
+
* @returns The .sdPlugin directory path, or `null` if unresolvable.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolvePluginDir(outDir: string): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Derive the CodePath (manifest entry point) from the bundler output
|
|
69
|
+
* file path, relative to the .sdPlugin directory.
|
|
70
|
+
*
|
|
71
|
+
* Example:
|
|
72
|
+
* outFile: "/project/com.example.sdPlugin/bin/plugin.mjs"
|
|
73
|
+
* pluginDir: "/project/com.example.sdPlugin"
|
|
74
|
+
* → "bin/plugin.mjs"
|
|
75
|
+
*/
|
|
76
|
+
export declare function deriveCodePath(outFile: string, pluginDir: string): string;
|
package/dist/bundler-shared.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { dirname, join, relative } from "node:path";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { copyFileSync, existsSync } from "node:fs";
|
|
4
4
|
//#region src/bundler-shared.ts
|
|
@@ -155,5 +155,40 @@ function copyNativeBindings(outDir, isDevelopment, options, warn) {
|
|
|
155
155
|
throw new Error(`[@fcannizzaro/streamdeck-react] Failed to copy native binding: ${String(err)}`);
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolve the .sdPlugin directory root from the bundler output directory.
|
|
160
|
+
*
|
|
161
|
+
* Strategy:
|
|
162
|
+
* 1. Walk up from outDir looking for a parent named `*.sdPlugin`
|
|
163
|
+
* 2. If not found, assume outDir's parent is the .sdPlugin root
|
|
164
|
+
* (handles `<uuid>.sdPlugin/bin/` → `<uuid>.sdPlugin/`)
|
|
165
|
+
*
|
|
166
|
+
* @returns The .sdPlugin directory path, or `null` if unresolvable.
|
|
167
|
+
*/
|
|
168
|
+
function resolvePluginDir(outDir) {
|
|
169
|
+
let dir = outDir;
|
|
170
|
+
const root = dirname(dir);
|
|
171
|
+
for (let i = 0; i < 4; i++) {
|
|
172
|
+
if (dir.endsWith(".sdPlugin")) return dir;
|
|
173
|
+
const parent = dirname(dir);
|
|
174
|
+
if (parent === dir) break;
|
|
175
|
+
dir = parent;
|
|
176
|
+
}
|
|
177
|
+
const parent = dirname(outDir);
|
|
178
|
+
if (parent !== root && parent.endsWith(".sdPlugin")) return parent;
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Derive the CodePath (manifest entry point) from the bundler output
|
|
183
|
+
* file path, relative to the .sdPlugin directory.
|
|
184
|
+
*
|
|
185
|
+
* Example:
|
|
186
|
+
* outFile: "/project/com.example.sdPlugin/bin/plugin.mjs"
|
|
187
|
+
* pluginDir: "/project/com.example.sdPlugin"
|
|
188
|
+
* → "bin/plugin.mjs"
|
|
189
|
+
*/
|
|
190
|
+
function deriveCodePath(outFile, pluginDir) {
|
|
191
|
+
return relative(pluginDir, outFile).replace(/\\/g, "/");
|
|
192
|
+
}
|
|
158
193
|
//#endregion
|
|
159
|
-
export { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, TAKUMI_NATIVE_LOADER_CODE, TAKUMI_NATIVE_LOADER_ID, copyNativeBindings, isDevelopmentMode, isLibraryDevtoolsImport, shouldStripDevtools };
|
|
194
|
+
export { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, TAKUMI_NATIVE_LOADER_CODE, TAKUMI_NATIVE_LOADER_ID, copyNativeBindings, deriveCodePath, isDevelopmentMode, isLibraryDevtoolsImport, resolvePluginDir, shouldStripDevtools };
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export { ErrorBoundary } from './components/ErrorBoundary';
|
|
|
25
25
|
export { tw } from './tw/index';
|
|
26
26
|
export { googleFont } from './google-font';
|
|
27
27
|
export type { GoogleFontVariant } from './google-font';
|
|
28
|
-
export type { PluginConfig, Plugin, FontConfig, ActionConfig,
|
|
28
|
+
export type { PluginConfig, Plugin, FontConfig, ActionConfig, ActionDefinition, EncoderLayout, WrapperComponent, TakumiBackend, Controller, Coordinates, Size, DeviceInfo, ActionInfo, CanvasInfo, TouchStripLayout, TouchStripLayoutItem, KeyDownPayload, KeyUpPayload, DialRotatePayload, DialPressPayload, TouchTapPayload, DialHints, StreamDeckAccess, TouchStripInfo, TouchStripTapPayload, TouchStripDialRotatePayload, TouchStripDialPressPayload, } from './types';
|
|
29
|
+
export type { PluginManifestInfo, ActionManifestInfo, ManifestController, ManifestEncoderInfo, ManifestTriggerDescription, ManifestStateInfo, ManifestOSInfo, ManifestNodejsInfo, ManifestProfileInfo, } from './manifest-types';
|
|
29
30
|
export type { TapOptions, LongPressOptions, DoubleTapOptions } from './hooks/gestures';
|
|
30
31
|
export type { BoxProps } from './components/Box';
|
|
31
32
|
export type { TextProps } from './components/Text';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ActionManifestInfo, ManifestActionSource } from './manifest-types';
|
|
2
|
+
type ASTNode = Record<string, any>;
|
|
3
|
+
export interface ExtractedAction {
|
|
4
|
+
uuid: string;
|
|
5
|
+
hasKey: boolean;
|
|
6
|
+
hasDial: boolean;
|
|
7
|
+
hasTouchStrip: boolean;
|
|
8
|
+
info: ActionManifestInfo | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extract all `defineAction()` calls from an ESTree-compatible AST.
|
|
12
|
+
*
|
|
13
|
+
* Walks the entire AST looking for `defineAction({ uuid, key?, dial?,
|
|
14
|
+
* touchStrip?, info? })` patterns and returns the extracted metadata.
|
|
15
|
+
*
|
|
16
|
+
* Actions with `info.disabled: true` are included in the results but
|
|
17
|
+
* can be filtered by the caller.
|
|
18
|
+
*
|
|
19
|
+
* @param ast - ESTree-compatible AST (from Rollup's `this.parse()` or `moduleParsed`)
|
|
20
|
+
* @returns Array of extracted action metadata
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractActionsFromAST(ast: ASTNode): ExtractedAction[];
|
|
23
|
+
/**
|
|
24
|
+
* Convert an ExtractedAction to a ManifestActionSource for the
|
|
25
|
+
* manifest generation engine.
|
|
26
|
+
*
|
|
27
|
+
* The `key`/`dial`/`touchStrip` fields are set to `true` (truthy)
|
|
28
|
+
* when the corresponding property was present in the defineAction()
|
|
29
|
+
* call, enabling the controller derivation logic in manifest-gen.ts.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractedToActionSource(extracted: ExtractedAction): ManifestActionSource;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region src/manifest-extract.ts
|
|
2
|
+
var SKIP_KEYS = new Set([
|
|
3
|
+
"type",
|
|
4
|
+
"start",
|
|
5
|
+
"end",
|
|
6
|
+
"loc",
|
|
7
|
+
"range"
|
|
8
|
+
]);
|
|
9
|
+
function walkAST(node, visitor) {
|
|
10
|
+
if (!node || typeof node !== "object" || !node.type) return;
|
|
11
|
+
visitor(node);
|
|
12
|
+
for (const key of Object.keys(node)) {
|
|
13
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
14
|
+
const child = node[key];
|
|
15
|
+
if (Array.isArray(child)) {
|
|
16
|
+
for (const item of child) if (item && typeof item === "object" && item.type) walkAST(item, visitor);
|
|
17
|
+
} else if (child && typeof child === "object" && child.type) walkAST(child, visitor);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
var UNEVALUABLE = Symbol("unevaluable");
|
|
21
|
+
function evaluateStatic(node) {
|
|
22
|
+
if (!node) return UNEVALUABLE;
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case "Literal": return node.value;
|
|
25
|
+
case "ObjectExpression": {
|
|
26
|
+
const obj = {};
|
|
27
|
+
for (const prop of node.properties ?? []) {
|
|
28
|
+
if (prop.type === "SpreadElement") return UNEVALUABLE;
|
|
29
|
+
if (prop.type !== "Property") continue;
|
|
30
|
+
if (prop.computed) return UNEVALUABLE;
|
|
31
|
+
const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.value;
|
|
32
|
+
if (typeof key !== "string") return UNEVALUABLE;
|
|
33
|
+
const value = evaluateStatic(prop.value);
|
|
34
|
+
if (value === UNEVALUABLE) return UNEVALUABLE;
|
|
35
|
+
obj[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return obj;
|
|
38
|
+
}
|
|
39
|
+
case "ArrayExpression": {
|
|
40
|
+
const arr = [];
|
|
41
|
+
for (const el of node.elements ?? []) {
|
|
42
|
+
if (!el) {
|
|
43
|
+
arr.push(null);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (el.type === "SpreadElement") return UNEVALUABLE;
|
|
47
|
+
const value = evaluateStatic(el);
|
|
48
|
+
if (value === UNEVALUABLE) return UNEVALUABLE;
|
|
49
|
+
arr.push(value);
|
|
50
|
+
}
|
|
51
|
+
return arr;
|
|
52
|
+
}
|
|
53
|
+
case "UnaryExpression":
|
|
54
|
+
if (node.operator === "-" && node.argument?.type === "Literal" && typeof node.argument.value === "number") return -node.argument.value;
|
|
55
|
+
if (node.operator === "!") {
|
|
56
|
+
const arg = evaluateStatic(node.argument);
|
|
57
|
+
if (arg === UNEVALUABLE) return UNEVALUABLE;
|
|
58
|
+
return !arg;
|
|
59
|
+
}
|
|
60
|
+
return UNEVALUABLE;
|
|
61
|
+
case "TemplateLiteral":
|
|
62
|
+
if (node.expressions?.length === 0 && node.quasis?.length === 1) return node.quasis[0]?.value?.cooked;
|
|
63
|
+
return UNEVALUABLE;
|
|
64
|
+
default: return UNEVALUABLE;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isDefineActionCall(node) {
|
|
68
|
+
if (node.type !== "CallExpression") return false;
|
|
69
|
+
const callee = node.callee;
|
|
70
|
+
return callee?.type === "Identifier" && callee.name === "defineAction";
|
|
71
|
+
}
|
|
72
|
+
function extractFromDefineAction(node) {
|
|
73
|
+
const args = node.arguments;
|
|
74
|
+
if (!args || args.length === 0) return null;
|
|
75
|
+
const arg = args[0];
|
|
76
|
+
if (arg.type !== "ObjectExpression") return null;
|
|
77
|
+
const propMap = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const prop of arg.properties ?? []) {
|
|
79
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
80
|
+
const key = prop.key?.type === "Identifier" ? prop.key.name : prop.key?.value;
|
|
81
|
+
if (typeof key === "string") propMap.set(key, prop.value);
|
|
82
|
+
}
|
|
83
|
+
const uuidNode = propMap.get("uuid");
|
|
84
|
+
if (!uuidNode || uuidNode.type !== "Literal" || typeof uuidNode.value !== "string") return null;
|
|
85
|
+
const infoNode = propMap.get("info");
|
|
86
|
+
let info = null;
|
|
87
|
+
if (infoNode) {
|
|
88
|
+
const evaluated = evaluateStatic(infoNode);
|
|
89
|
+
if (evaluated !== UNEVALUABLE && typeof evaluated === "object" && evaluated !== null) {
|
|
90
|
+
const candidate = evaluated;
|
|
91
|
+
if (typeof candidate.name === "string" && typeof candidate.icon === "string") info = candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
uuid: uuidNode.value,
|
|
96
|
+
hasKey: propMap.has("key"),
|
|
97
|
+
hasDial: propMap.has("dial"),
|
|
98
|
+
hasTouchStrip: propMap.has("touchStrip"),
|
|
99
|
+
info
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract all `defineAction()` calls from an ESTree-compatible AST.
|
|
104
|
+
*
|
|
105
|
+
* Walks the entire AST looking for `defineAction({ uuid, key?, dial?,
|
|
106
|
+
* touchStrip?, info? })` patterns and returns the extracted metadata.
|
|
107
|
+
*
|
|
108
|
+
* Actions with `info.disabled: true` are included in the results but
|
|
109
|
+
* can be filtered by the caller.
|
|
110
|
+
*
|
|
111
|
+
* @param ast - ESTree-compatible AST (from Rollup's `this.parse()` or `moduleParsed`)
|
|
112
|
+
* @returns Array of extracted action metadata
|
|
113
|
+
*/
|
|
114
|
+
function extractActionsFromAST(ast) {
|
|
115
|
+
const results = [];
|
|
116
|
+
walkAST(ast, (node) => {
|
|
117
|
+
if (!isDefineActionCall(node)) return;
|
|
118
|
+
const extracted = extractFromDefineAction(node);
|
|
119
|
+
if (extracted) results.push(extracted);
|
|
120
|
+
});
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Convert an ExtractedAction to a ManifestActionSource for the
|
|
125
|
+
* manifest generation engine.
|
|
126
|
+
*
|
|
127
|
+
* The `key`/`dial`/`touchStrip` fields are set to `true` (truthy)
|
|
128
|
+
* when the corresponding property was present in the defineAction()
|
|
129
|
+
* call, enabling the controller derivation logic in manifest-gen.ts.
|
|
130
|
+
*/
|
|
131
|
+
function extractedToActionSource(extracted) {
|
|
132
|
+
return {
|
|
133
|
+
uuid: extracted.uuid,
|
|
134
|
+
key: extracted.hasKey ? true : void 0,
|
|
135
|
+
dial: extracted.hasDial ? true : void 0,
|
|
136
|
+
touchStrip: extracted.hasTouchStrip ? true : void 0,
|
|
137
|
+
info: extracted.info ?? void 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { extractActionsFromAST, extractedToActionSource };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ManifestConfig } from './manifest-types';
|
|
2
|
+
export interface ManifestValidationError {
|
|
3
|
+
field: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Validate just the plugin UUID format.
|
|
8
|
+
*
|
|
9
|
+
* Used in `buildStart` for early error reporting before action
|
|
10
|
+
* extraction is complete.
|
|
11
|
+
*
|
|
12
|
+
* @returns A validation error if the UUID is invalid, or `null` if valid.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validatePluginUUID(uuid: string): ManifestValidationError | null;
|
|
15
|
+
/**
|
|
16
|
+
* Validate a full ManifestConfig for correctness.
|
|
17
|
+
*
|
|
18
|
+
* Checks:
|
|
19
|
+
* - All action UUIDs are prefixed with the plugin UUID
|
|
20
|
+
* - No duplicate action UUIDs
|
|
21
|
+
* - Plugin UUID matches reverse-DNS pattern
|
|
22
|
+
*
|
|
23
|
+
* Called in `writeBundle` after action extraction is complete.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateManifestConfig(config: ManifestConfig, warn?: (msg: string) => void): ManifestValidationError[];
|
|
26
|
+
type JsonRecord = Record<string, any>;
|
|
27
|
+
/**
|
|
28
|
+
* Build the full manifest JSON object from a ManifestConfig.
|
|
29
|
+
*
|
|
30
|
+
* Applies all auto-derivation defaults and transforms camelCase
|
|
31
|
+
* to the PascalCase format expected by the Elgato schema.
|
|
32
|
+
*
|
|
33
|
+
* @param config - The manifest configuration
|
|
34
|
+
* @param codePath - Override CodePath (typically derived from bundler output)
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildManifestJson(config: ManifestConfig, codePath?: string): JsonRecord;
|
|
37
|
+
/**
|
|
38
|
+
* Generate the full manifest JSON string from a ManifestConfig.
|
|
39
|
+
*
|
|
40
|
+
* @param config - The manifest configuration
|
|
41
|
+
* @param codePath - Override CodePath (typically derived from bundler output)
|
|
42
|
+
* @returns Formatted JSON string
|
|
43
|
+
*/
|
|
44
|
+
export declare function generateManifestJsonString(config: ManifestConfig, codePath?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Write manifest.json only when the content has changed.
|
|
47
|
+
* Creates the parent directory if it does not exist.
|
|
48
|
+
*
|
|
49
|
+
* @returns `true` if the file was written, `false` if content was unchanged.
|
|
50
|
+
*/
|
|
51
|
+
export declare function writeManifestIfChanged(outPath: string, content: string): boolean;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
//#region src/manifest-gen.ts
|
|
4
|
+
/**
|
|
5
|
+
* Validate just the plugin UUID format.
|
|
6
|
+
*
|
|
7
|
+
* Used in `buildStart` for early error reporting before action
|
|
8
|
+
* extraction is complete.
|
|
9
|
+
*
|
|
10
|
+
* @returns A validation error if the UUID is invalid, or `null` if valid.
|
|
11
|
+
*/
|
|
12
|
+
function validatePluginUUID(uuid) {
|
|
13
|
+
if (!/^([a-z0-9-]+)(\.[a-z0-9-]+)+$/.test(uuid)) return {
|
|
14
|
+
field: "uuid",
|
|
15
|
+
message: `Plugin UUID "${uuid}" must be in reverse-DNS format (lowercase alphanumeric, hyphens, periods)`
|
|
16
|
+
};
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate a full ManifestConfig for correctness.
|
|
21
|
+
*
|
|
22
|
+
* Checks:
|
|
23
|
+
* - All action UUIDs are prefixed with the plugin UUID
|
|
24
|
+
* - No duplicate action UUIDs
|
|
25
|
+
* - Plugin UUID matches reverse-DNS pattern
|
|
26
|
+
*
|
|
27
|
+
* Called in `writeBundle` after action extraction is complete.
|
|
28
|
+
*/
|
|
29
|
+
function validateManifestConfig(config, warn) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
const uuidError = validatePluginUUID(config.uuid);
|
|
32
|
+
if (uuidError) errors.push(uuidError);
|
|
33
|
+
const prefix = config.uuid + ".";
|
|
34
|
+
const seenUuids = /* @__PURE__ */ new Set();
|
|
35
|
+
for (const action of config.actions) {
|
|
36
|
+
if (!action.uuid.startsWith(prefix)) {
|
|
37
|
+
const error = {
|
|
38
|
+
field: `actions[${action.uuid}].uuid`,
|
|
39
|
+
message: `Action UUID "${action.uuid}" must be prefixed with plugin UUID "${config.uuid}."`
|
|
40
|
+
};
|
|
41
|
+
errors.push(error);
|
|
42
|
+
warn?.(`[@fcannizzaro/streamdeck-react] ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (seenUuids.has(action.uuid)) errors.push({
|
|
45
|
+
field: `actions[${action.uuid}].uuid`,
|
|
46
|
+
message: `Duplicate action UUID "${action.uuid}"`
|
|
47
|
+
});
|
|
48
|
+
seenUuids.add(action.uuid);
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
51
|
+
}
|
|
52
|
+
/** Add a key to the record only if the value is defined. */
|
|
53
|
+
function addIf(record, key, value) {
|
|
54
|
+
if (value !== void 0 && value !== null) record[key] = value;
|
|
55
|
+
}
|
|
56
|
+
function buildTriggerDescription(td) {
|
|
57
|
+
const out = {};
|
|
58
|
+
addIf(out, "Rotate", td.rotate);
|
|
59
|
+
addIf(out, "Push", td.push);
|
|
60
|
+
addIf(out, "Touch", td.touch);
|
|
61
|
+
addIf(out, "LongTouch", td.longTouch);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function buildEncoder(encoder) {
|
|
65
|
+
const out = {};
|
|
66
|
+
addIf(out, "layout", encoder.layout);
|
|
67
|
+
addIf(out, "Icon", encoder.icon);
|
|
68
|
+
addIf(out, "StackColor", encoder.stackColor);
|
|
69
|
+
addIf(out, "background", encoder.background);
|
|
70
|
+
if (encoder.triggerDescription) out["TriggerDescription"] = buildTriggerDescription(encoder.triggerDescription);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function buildState(state) {
|
|
74
|
+
const out = { Image: state.image };
|
|
75
|
+
addIf(out, "Name", state.name);
|
|
76
|
+
addIf(out, "Title", state.title);
|
|
77
|
+
addIf(out, "ShowTitle", state.showTitle);
|
|
78
|
+
addIf(out, "TitleAlignment", state.titleAlignment);
|
|
79
|
+
addIf(out, "TitleColor", state.titleColor);
|
|
80
|
+
addIf(out, "FontFamily", state.fontFamily);
|
|
81
|
+
addIf(out, "FontSize", state.fontSize);
|
|
82
|
+
addIf(out, "FontStyle", state.fontStyle);
|
|
83
|
+
addIf(out, "FontUnderline", state.fontUnderline);
|
|
84
|
+
addIf(out, "MultiActionImage", state.multiActionImage);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Derive Controllers from an action definition.
|
|
89
|
+
*
|
|
90
|
+
* Priority:
|
|
91
|
+
* 1. Explicit `controllers` on info → use as-is
|
|
92
|
+
* 2. Derived from key/dial/touchStrip presence:
|
|
93
|
+
* - key → includes "Keypad"
|
|
94
|
+
* - dial or touchStrip → includes "Encoder"
|
|
95
|
+
* - both → ["Keypad", "Encoder"]
|
|
96
|
+
* 3. Default → ["Keypad"]
|
|
97
|
+
*/
|
|
98
|
+
function deriveControllers(action) {
|
|
99
|
+
const info = action.info;
|
|
100
|
+
if (info?.controllers) return info.controllers.filter((c) => c != null);
|
|
101
|
+
const hasKey = action.key != null;
|
|
102
|
+
const hasEncoder = action.dial != null || action.touchStrip != null;
|
|
103
|
+
if (hasKey && hasEncoder) return ["Keypad", "Encoder"];
|
|
104
|
+
if (hasEncoder) return ["Encoder"];
|
|
105
|
+
if (hasKey) return ["Keypad"];
|
|
106
|
+
if (info?.encoder) return ["Encoder"];
|
|
107
|
+
return ["Keypad"];
|
|
108
|
+
}
|
|
109
|
+
function buildAction(action) {
|
|
110
|
+
const info = action.info;
|
|
111
|
+
if (!info) throw new Error(`[@fcannizzaro/streamdeck-react] Action "${action.uuid}" is missing \`info\`. Add info: { name, icon } to the defineAction() call.`);
|
|
112
|
+
const controllers = deriveControllers(action);
|
|
113
|
+
const states = info.states ? info.states.map(buildState) : [{ Image: info.icon }];
|
|
114
|
+
const out = {
|
|
115
|
+
UUID: action.uuid,
|
|
116
|
+
Name: info.name,
|
|
117
|
+
Icon: info.icon,
|
|
118
|
+
Controllers: controllers,
|
|
119
|
+
States: states
|
|
120
|
+
};
|
|
121
|
+
addIf(out, "Tooltip", info.tooltip);
|
|
122
|
+
addIf(out, "DisableAutomaticStates", info.disableAutomaticStates);
|
|
123
|
+
addIf(out, "DisableCaching", info.disableCaching);
|
|
124
|
+
addIf(out, "SupportedInMultiActions", info.supportedInMultiActions);
|
|
125
|
+
addIf(out, "SupportedInKeyLogicActions", info.supportedInKeyLogicActions);
|
|
126
|
+
addIf(out, "VisibleInActionsList", info.visibleInActionsList);
|
|
127
|
+
addIf(out, "UserTitleEnabled", info.userTitleEnabled);
|
|
128
|
+
addIf(out, "PropertyInspectorPath", info.propertyInspectorPath);
|
|
129
|
+
addIf(out, "SupportURL", info.supportUrl);
|
|
130
|
+
if (info.encoder) out["Encoder"] = buildEncoder(info.encoder);
|
|
131
|
+
if (info.os) out["OS"] = info.os;
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
var DEFAULT_OS = [{
|
|
135
|
+
platform: "mac",
|
|
136
|
+
minimumVersion: "13"
|
|
137
|
+
}, {
|
|
138
|
+
platform: "windows",
|
|
139
|
+
minimumVersion: "10"
|
|
140
|
+
}];
|
|
141
|
+
function buildOS(osEntries) {
|
|
142
|
+
return osEntries.filter((o) => o != null).map((o) => ({
|
|
143
|
+
Platform: o.platform,
|
|
144
|
+
MinimumVersion: o.minimumVersion
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
function buildNodejs(nodejs) {
|
|
148
|
+
const out = { Version: nodejs.version };
|
|
149
|
+
addIf(out, "Debug", nodejs.debug);
|
|
150
|
+
addIf(out, "GenerateProfilerOutput", nodejs.generateProfilerOutput);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
function buildProfile(profile) {
|
|
154
|
+
const out = {
|
|
155
|
+
Name: profile.name,
|
|
156
|
+
DeviceType: profile.deviceType
|
|
157
|
+
};
|
|
158
|
+
addIf(out, "AutoInstall", profile.autoInstall);
|
|
159
|
+
addIf(out, "DontAutoSwitchWhenInstalled", profile.dontAutoSwitchWhenInstalled);
|
|
160
|
+
addIf(out, "Readonly", profile.readonly);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build the full manifest JSON object from a ManifestConfig.
|
|
165
|
+
*
|
|
166
|
+
* Applies all auto-derivation defaults and transforms camelCase
|
|
167
|
+
* to the PascalCase format expected by the Elgato schema.
|
|
168
|
+
*
|
|
169
|
+
* @param config - The manifest configuration
|
|
170
|
+
* @param codePath - Override CodePath (typically derived from bundler output)
|
|
171
|
+
*/
|
|
172
|
+
function buildManifestJson(config, codePath) {
|
|
173
|
+
const osEntries = config.os ? config.os.filter((o) => o != null) : DEFAULT_OS;
|
|
174
|
+
const nodejs = config.nodejs ?? { version: "24" };
|
|
175
|
+
const out = {
|
|
176
|
+
$schema: "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
|
|
177
|
+
UUID: config.uuid,
|
|
178
|
+
Name: config.name,
|
|
179
|
+
Author: config.author,
|
|
180
|
+
Description: config.description,
|
|
181
|
+
Icon: config.icon,
|
|
182
|
+
Version: config.version,
|
|
183
|
+
CodePath: codePath ?? config.codePath ?? "bin/plugin.mjs",
|
|
184
|
+
OS: buildOS(osEntries),
|
|
185
|
+
Nodejs: buildNodejs(nodejs),
|
|
186
|
+
SDKVersion: config.sdkVersion ?? 2,
|
|
187
|
+
Software: { MinimumVersion: config.software?.minimumVersion ?? "7.1" },
|
|
188
|
+
Category: config.category ?? config.name,
|
|
189
|
+
CategoryIcon: config.categoryIcon ?? config.icon,
|
|
190
|
+
Actions: config.actions.map(buildAction)
|
|
191
|
+
};
|
|
192
|
+
addIf(out, "URL", config.url);
|
|
193
|
+
addIf(out, "SupportURL", config.supportUrl);
|
|
194
|
+
addIf(out, "PropertyInspectorPath", config.propertyInspectorPath);
|
|
195
|
+
addIf(out, "DefaultWindowSize", config.defaultWindowSize);
|
|
196
|
+
addIf(out, "CodePathMac", config.codePathMac);
|
|
197
|
+
addIf(out, "CodePathWin", config.codePathWin);
|
|
198
|
+
if (config.applicationsToMonitor) out["ApplicationsToMonitor"] = config.applicationsToMonitor;
|
|
199
|
+
if (config.profiles?.length) out["Profiles"] = config.profiles.map(buildProfile);
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Generate the full manifest JSON string from a ManifestConfig.
|
|
204
|
+
*
|
|
205
|
+
* @param config - The manifest configuration
|
|
206
|
+
* @param codePath - Override CodePath (typically derived from bundler output)
|
|
207
|
+
* @returns Formatted JSON string
|
|
208
|
+
*/
|
|
209
|
+
function generateManifestJsonString(config, codePath) {
|
|
210
|
+
const json = buildManifestJson(config, codePath);
|
|
211
|
+
return JSON.stringify(json, null, 2) + "\n";
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Write manifest.json only when the content has changed.
|
|
215
|
+
* Creates the parent directory if it does not exist.
|
|
216
|
+
*
|
|
217
|
+
* @returns `true` if the file was written, `false` if content was unchanged.
|
|
218
|
+
*/
|
|
219
|
+
function writeManifestIfChanged(outPath, content) {
|
|
220
|
+
if (existsSync(outPath)) {
|
|
221
|
+
if (readFileSync(outPath, "utf-8") === content) return false;
|
|
222
|
+
}
|
|
223
|
+
const dir = dirname(outPath);
|
|
224
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
225
|
+
writeFileSync(outPath, content);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
export { generateManifestJsonString, validateManifestConfig, validatePluginUUID, writeManifestIfChanged };
|