@fcannizzaro/streamdeck-react 0.1.12 → 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/README.md CHANGED
@@ -63,11 +63,13 @@ export const counterAction = defineAction({
63
63
  Register it in your plugin entrypoint:
64
64
 
65
65
  ```ts
66
- import { createPlugin } from "@fcannizzaro/streamdeck-react";
66
+ import { createPlugin, googleFont } from "@fcannizzaro/streamdeck-react";
67
67
  import { counterAction } from "./actions/counter.tsx";
68
68
 
69
+ const inter = await googleFont("Inter");
70
+
69
71
  const plugin = createPlugin({
70
- fonts: [],
72
+ fonts: [inter],
71
73
  actions: [counterAction],
72
74
  });
73
75
 
@@ -83,12 +85,13 @@ await plugin.connect();
83
85
 
84
86
  ## Samples
85
87
 
86
- - `samples/counter/` - local state, settings hook, dial interaction
87
- - `samples/zustand/` - shared external store across multiple actions
88
- - `samples/jotai/` - shared atom state with a plugin-level wrapper
89
- - `samples/pokemon/` - richer plugin example with custom wrappers
90
- - `samples/snake/` - snake game on the Stream Deck+ touch strip using dial controls and touch tap
91
- - `samples/weather/` - weather forecast dials with animated detail panels and a shared Zustand store
88
+ - `samples/counter/` local state, persisted settings, dial interaction
89
+ - `samples/zustand/` shared state across keys via a module-scope Zustand store
90
+ - `samples/jotai/` shared atom state with a plugin-level Jotai Provider wrapper
91
+ - `samples/pokemon/` data fetching with TanStack Query and remote image rendering
92
+ - `samples/animation/` spring bounce, fade-slide, and spring dial animations
93
+ - `samples/snake/` snake game on the Stream Deck+ TouchStrip using dial controls and touch tap
94
+ - `samples/weather/` — weather forecast dials with animated detail panels and a shared Zustand store
92
95
 
93
96
  ## DevTools
94
97
 
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
@@ -4,8 +4,16 @@ export interface StreamDeckTarget {
4
4
  platform: StreamDeckPlatform;
5
5
  arch: StreamDeckArch;
6
6
  }
7
+ /** Takumi renderer backend selection. Mirrors the runtime `TakumiBackend` type for build-time configuration. */
8
+ export type TakumiBackend = "native-binding" | "wasm";
7
9
  export interface StreamDeckTargetOptions {
8
10
  targets?: StreamDeckTarget[];
11
+ /**
12
+ * Takumi renderer backend. When `"wasm"`, native `.node` binding
13
+ * copying is skipped entirely during the build.
14
+ * @default "native-binding"
15
+ */
16
+ takumi?: TakumiBackend;
9
17
  }
10
18
  export interface ResolvedTarget extends StreamDeckTarget {
11
19
  pkg: string;
@@ -17,6 +25,8 @@ export declare function isArch(value: string): value is StreamDeckArch;
17
25
  export declare function isDevelopmentMode(watchMode: boolean | undefined): boolean;
18
26
  export declare const NOOP_DEVTOOLS_ID = "\0streamdeck-react:noop-devtools";
19
27
  export declare const NOOP_DEVTOOLS_CODE = "export function startDevtoolsServer() {}";
28
+ export declare const TAKUMI_NATIVE_LOADER_ID = "\0streamdeck-react:takumi-native";
29
+ export declare const TAKUMI_NATIVE_LOADER_CODE: string;
20
30
  /**
21
31
  * Returns true when devtools should be stripped from the bundle.
22
32
  * Only strips in explicit production mode (NODE_ENV=production) to avoid
@@ -43,3 +53,24 @@ export declare function expandTargets(targets: StreamDeckTarget[]): ResolvedTarg
43
53
  * In production: missing bindings throw an error (the plugin won't work).
44
54
  */
45
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
@@ -40,6 +40,32 @@ function isDevelopmentMode(watchMode) {
40
40
  var NOOP_DEVTOOLS_ID = "\0streamdeck-react:noop-devtools";
41
41
  var NOOP_DEVTOOLS_CODE = "export function startDevtoolsServer() {}";
42
42
  var DEVTOOLS_IMPORT_SOURCE = "./devtools/index.js";
43
+ var TAKUMI_NATIVE_LOADER_ID = "\0streamdeck-react:takumi-native";
44
+ var TAKUMI_NATIVE_LOADER_CODE = `
45
+ import { createRequire } from "node:module";
46
+ const require = createRequire(import.meta.url);
47
+ let binding = null;
48
+ if (process.platform === "darwin") {
49
+ if (process.arch === "arm64") {
50
+ try { binding = require("./core.darwin-arm64.node"); } catch {}
51
+ } else if (process.arch === "x64") {
52
+ try { binding = require("./core.darwin-x64.node"); } catch {}
53
+ }
54
+ } else if (process.platform === "win32") {
55
+ if (process.arch === "arm64") {
56
+ try { binding = require("./core.win32-arm64-msvc.node"); } catch {}
57
+ } else if (process.arch === "x64") {
58
+ try { binding = require("./core.win32-x64-msvc.node"); } catch {}
59
+ }
60
+ }
61
+ if (!binding) {
62
+ throw new Error(
63
+ "[@fcannizzaro/streamdeck-react] Failed to load @takumi-rs/core native binding for " +
64
+ process.platform + "-" + process.arch
65
+ );
66
+ }
67
+ export const { Renderer, OutputFormat, DitheringAlgorithm, AnimationOutputFormat, extractResourceUrls } = binding;
68
+ `.trim();
43
69
  /**
44
70
  * Returns true when devtools should be stripped from the bundle.
45
71
  * Only strips in explicit production mode (NODE_ENV=production) to avoid
@@ -92,6 +118,7 @@ function expandTargets(targets) {
92
118
  * In production: missing bindings throw an error (the plugin won't work).
93
119
  */
94
120
  function copyNativeBindings(outDir, isDevelopment, options, warn) {
121
+ if (options.takumi === "wasm") return;
95
122
  try {
96
123
  const requestedTargets = normalizeTargetRequests(options, isDevelopment);
97
124
  if (requestedTargets.length === 0) {
@@ -128,5 +155,40 @@ function copyNativeBindings(outDir, isDevelopment, options, warn) {
128
155
  throw new Error(`[@fcannizzaro/streamdeck-react] Failed to copy native binding: ${String(err)}`);
129
156
  }
130
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
+ }
131
193
  //#endregion
132
- export { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_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 };
@@ -1,7 +1,7 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { dirname, resolve } from "node:path";
2
3
  import { createRequire } from "node:module";
3
4
  import { realpathSync } from "node:fs";
4
- import { readFile } from "node:fs/promises";
5
5
  //#region src/font-inline.ts
6
6
  var FONT_RE = /\.(ttf|otf|woff2?)$/;
7
7
  /**
@@ -0,0 +1,61 @@
1
+ import { FontConfig } from './types';
2
+ type FontWeight = FontConfig["weight"];
3
+ type FontStyle = FontConfig["style"];
4
+ export interface GoogleFontVariant {
5
+ weight?: FontWeight;
6
+ style?: FontStyle;
7
+ }
8
+ /**
9
+ * Fetch a Google Font as a ready-to-use `FontConfig`.
10
+ *
11
+ * Downloads TTF font data directly from Google Fonts — no npm package
12
+ * needed. The returned config can be passed straight into
13
+ * `createPlugin({ fonts: [...] })`.
14
+ *
15
+ * Font files are cached to `.google-fonts/` in the current working
16
+ * directory. Subsequent calls read from disk without network access.
17
+ *
18
+ * @param family - The Google Font family name (e.g. `"Inter"`, `"Roboto"`).
19
+ * @returns A `FontConfig` with weight 400 and normal style.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const inter = await googleFont("Inter");
24
+ * createPlugin({ fonts: [inter], actions: [...] });
25
+ * ```
26
+ */
27
+ export declare function googleFont(family: string): Promise<FontConfig>;
28
+ /**
29
+ * Fetch a specific weight/style variant of a Google Font.
30
+ *
31
+ * @param family - The Google Font family name.
32
+ * @param variant - Weight and/or style to fetch.
33
+ * @returns A single `FontConfig`.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const bold = await googleFont("Inter", { weight: 700 });
38
+ * ```
39
+ */
40
+ export declare function googleFont(family: string, variant: GoogleFontVariant): Promise<FontConfig>;
41
+ /**
42
+ * Fetch multiple weight/style variants of a Google Font in one call.
43
+ *
44
+ * All variants are fetched in parallel for maximum throughput.
45
+ *
46
+ * @param family - The Google Font family name.
47
+ * @param variants - Array of weight/style combinations to fetch.
48
+ * @returns An array of `FontConfig` objects, one per variant.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const fonts = await googleFont("Inter", [
53
+ * { weight: 400 },
54
+ * { weight: 700 },
55
+ * { weight: 700, style: "italic" },
56
+ * ]);
57
+ * createPlugin({ fonts, actions: [...] });
58
+ * ```
59
+ */
60
+ export declare function googleFont(family: string, variants: GoogleFontVariant[]): Promise<FontConfig[]>;
61
+ export {};
@@ -0,0 +1,124 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ //#region src/google-font.ts
4
+ var TTF_USER_AGENT = "Mozilla/4.0";
5
+ var GOOGLE_FONTS_CSS2_BASE = "https://fonts.googleapis.com/css2";
6
+ var CACHE_DIR = ".google-fonts";
7
+ function sanitizeName(family) {
8
+ return family.toLowerCase().replace(/\s+/g, "-");
9
+ }
10
+ function cacheFilePath(family, weight, style) {
11
+ return join(process.cwd(), CACHE_DIR, `${sanitizeName(family)}-${weight}-${style}.ttf`);
12
+ }
13
+ async function fileExists(path) {
14
+ try {
15
+ await access(path);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ async function ensureCacheDir() {
22
+ await mkdir(join(process.cwd(), CACHE_DIR), { recursive: true });
23
+ }
24
+ async function readCached(family, weight, style) {
25
+ const path = cacheFilePath(family, weight, style);
26
+ if (!await fileExists(path)) return null;
27
+ return {
28
+ name: family,
29
+ data: (await readFile(path)).buffer,
30
+ weight,
31
+ style
32
+ };
33
+ }
34
+ async function writeCache(family, weight, style, data) {
35
+ await ensureCacheDir();
36
+ await writeFile(cacheFilePath(family, weight, style), Buffer.from(data));
37
+ }
38
+ function buildCss2Url(family, variants) {
39
+ const hasItalic = variants.some((v) => v.style === "italic");
40
+ const encodedFamily = encodeURIComponent(family);
41
+ if (hasItalic) {
42
+ const specs = variants.map((v) => {
43
+ return `${v.style === "italic" ? 1 : 0},${v.weight ?? 400}`;
44
+ });
45
+ specs.sort();
46
+ return `${GOOGLE_FONTS_CSS2_BASE}?family=${encodedFamily}:ital,wght@${specs.join(";")}`;
47
+ }
48
+ const weights = variants.map((v) => v.weight ?? 400);
49
+ weights.sort((a, b) => a - b);
50
+ return `${GOOGLE_FONTS_CSS2_BASE}?family=${encodedFamily}:wght@${weights.join(";")}`;
51
+ }
52
+ function parseFontFaces(css) {
53
+ const results = [];
54
+ const seen = /* @__PURE__ */ new Set();
55
+ const blockRegex = /@font-face\s*\{([^}]+)\}/g;
56
+ let match;
57
+ while ((match = blockRegex.exec(css)) !== null) {
58
+ const block = match[1];
59
+ const urlMatch = block.match(/url\(([^)]+)\)/);
60
+ const weightMatch = block.match(/font-weight:\s*(\d+)/);
61
+ const styleMatch = block.match(/font-style:\s*(normal|italic)/);
62
+ if (!urlMatch?.[1]) continue;
63
+ const weight = Number(weightMatch?.[1] ?? 400);
64
+ const style = styleMatch?.[1] ?? "normal";
65
+ const key = `${weight}:${style}`;
66
+ if (seen.has(key)) continue;
67
+ seen.add(key);
68
+ results.push({
69
+ url: urlMatch[1],
70
+ weight,
71
+ style
72
+ });
73
+ }
74
+ return results;
75
+ }
76
+ async function fetchGoogleFonts(family, variants) {
77
+ const results = [];
78
+ const uncached = [];
79
+ for (const v of variants) {
80
+ const weight = v.weight ?? 400;
81
+ const style = v.style ?? "normal";
82
+ const cached = await readCached(family, weight, style);
83
+ if (cached) results.push(cached);
84
+ else uncached.push({
85
+ weight,
86
+ style
87
+ });
88
+ }
89
+ if (uncached.length === 0) return results;
90
+ const url = buildCss2Url(family, uncached);
91
+ const cssResponse = await fetch(url, { headers: { "User-Agent": TTF_USER_AGENT } });
92
+ if (!cssResponse.ok) throw new Error(`Google Fonts: failed to fetch CSS for "${family}" (HTTP ${cssResponse.status})`);
93
+ const faces = parseFontFaces(await cssResponse.text());
94
+ if (faces.length === 0) throw new Error(`Google Fonts: no font faces found in CSS response for "${family}". Verify the family name is correct at https://fonts.google.com`);
95
+ const downloaded = await Promise.all(faces.map(async (face) => {
96
+ const fontResponse = await fetch(face.url);
97
+ if (!fontResponse.ok) throw new Error(`Google Fonts: failed to download font file for "${family}" weight ${face.weight} (HTTP ${fontResponse.status})`);
98
+ const data = await fontResponse.arrayBuffer();
99
+ writeCache(family, face.weight, face.style, data).catch(() => {});
100
+ return {
101
+ name: family,
102
+ data,
103
+ weight: face.weight,
104
+ style: face.style
105
+ };
106
+ }));
107
+ return [...results, ...downloaded];
108
+ }
109
+ async function googleFont(family, variantOrVariants) {
110
+ if (variantOrVariants === void 0) return (await fetchGoogleFonts(family, [{
111
+ weight: 400,
112
+ style: "normal"
113
+ }]))[0];
114
+ if (!Array.isArray(variantOrVariants)) return (await fetchGoogleFonts(family, [{
115
+ weight: variantOrVariants.weight ?? 400,
116
+ style: variantOrVariants.style ?? "normal"
117
+ }]))[0];
118
+ return fetchGoogleFonts(family, variantOrVariants.map((v) => ({
119
+ weight: v.weight ?? 400,
120
+ style: v.style ?? "normal"
121
+ })));
122
+ }
123
+ //#endregion
124
+ export { googleFont };
package/dist/index.d.ts CHANGED
@@ -23,7 +23,10 @@ export { ProgressBar } from './components/ProgressBar';
23
23
  export { CircularGauge } from './components/CircularGauge';
24
24
  export { ErrorBoundary } from './components/ErrorBoundary';
25
25
  export { tw } from './tw/index';
26
- export type { PluginConfig, FontConfig, ActionConfig, ActionConfigInput, ActionDefinition, ActionUUID, ManifestActions, EncoderLayout, WrapperComponent, Controller, Coordinates, Size, DeviceInfo, ActionInfo, CanvasInfo, TouchStripLayout, TouchStripLayoutItem, KeyDownPayload, KeyUpPayload, DialRotatePayload, DialPressPayload, TouchTapPayload, DialHints, StreamDeckAccess, TouchStripInfo, TouchStripTapPayload, TouchStripDialRotatePayload, TouchStripDialPressPayload, } from './types';
26
+ export { googleFont } from './google-font';
27
+ export type { GoogleFontVariant } from './google-font';
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';
27
30
  export type { TapOptions, LongPressOptions, DoubleTapOptions } from './hooks/gestures';
28
31
  export type { BoxProps } from './components/Box';
29
32
  export type { TextProps } from './components/Text';
package/dist/index.js CHANGED
@@ -18,4 +18,5 @@ import { ProgressBar } from "./components/ProgressBar.js";
18
18
  import { CircularGauge } from "./components/CircularGauge.js";
19
19
  import { ErrorBoundary } from "./components/ErrorBoundary.js";
20
20
  import { tw } from "./tw/index.js";
21
- export { Box, CircularGauge, Easings, ErrorBoundary, Icon, Image, ProgressBar, SpringPresets, Text, createPlugin, defineAction, physicalDevice, tw, useAction, useCanvas, useDevice, useDialDown, useDialHint, useDialRotate, useDialUp, useDoubleTap, useGlobalSettings, useInterval, useKeyDown, useKeyUp, useLongPress, useOpenUrl, usePrevious, usePropertyInspector, useSendToPI, useSettings, useShowAlert, useShowOk, useSpring, useStreamDeck, useSwitchProfile, useTap, useTick, useTimeout, useTitle, useTouchStrip, useTouchStripDialDown, useTouchStripDialRotate, useTouchStripDialUp, useTouchStripTap, useTouchTap, useTween, useWillAppear, useWillDisappear };
21
+ import { googleFont } from "./google-font.js";
22
+ export { Box, CircularGauge, Easings, ErrorBoundary, Icon, Image, ProgressBar, SpringPresets, Text, createPlugin, defineAction, googleFont, physicalDevice, tw, useAction, useCanvas, useDevice, useDialDown, useDialHint, useDialRotate, useDialUp, useDoubleTap, useGlobalSettings, useInterval, useKeyDown, useKeyUp, useLongPress, useOpenUrl, usePrevious, usePropertyInspector, useSendToPI, useSettings, useShowAlert, useShowOk, useSpring, useStreamDeck, useSwitchProfile, useTap, useTick, useTimeout, useTitle, useTouchStrip, useTouchStripDialDown, useTouchStripDialRotate, useTouchStripDialUp, useTouchStripTap, useTouchTap, useTween, useWillAppear, useWillDisappear };
@@ -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 {};