@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 CHANGED
@@ -1,3 +1,3 @@
1
- import { ActionConfigInput, ActionDefinition } from './types';
1
+ import { ActionConfig, ActionDefinition } from './types';
2
2
  import { JsonObject } from '@elgato/utils';
3
- export declare function defineAction<S extends JsonObject = JsonObject>(config: ActionConfigInput<S>): ActionDefinition<S>;
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
@@ -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;
@@ -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, ActionConfigInput, ActionDefinition, ActionUUID, ManifestActions, EncoderLayout, WrapperComponent, TakumiBackend, Controller, Coordinates, Size, DeviceInfo, ActionInfo, CanvasInfo, TouchStripLayout, TouchStripLayoutItem, KeyDownPayload, KeyUpPayload, DialRotatePayload, DialPressPayload, TouchTapPayload, DialHints, StreamDeckAccess, TouchStripInfo, TouchStripTapPayload, TouchStripDialRotatePayload, TouchStripDialPressPayload, } from './types';
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 };