refactor: added type generation from schema for sdk with comments

This commit is contained in:
Simeon Radivoev 2026-05-05 02:32:07 +03:00
parent 2683d46b16
commit 04e332d91e
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
7 changed files with 65 additions and 20 deletions

View file

@ -101,6 +101,7 @@
"vite-plugin-svg-icons-ng": "^1.5.2", "vite-plugin-svg-icons-ng": "^1.5.2",
"vite-static-assets-plugin": "^1.2.2", "vite-static-assets-plugin": "^1.2.2",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"zod-to-ts": "^2.0.0",
}, },
}, },
}, },
@ -1923,6 +1924,8 @@
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-to-ts": ["zod-to-ts@2.0.0", "", { "peerDependencies": { "typescript": "^5.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-aHsUgIl+CQutKAxtRNeZslLCLXoeuSq+j5HU7q3kvi/c2KIAo6q4YjT7/lwFfACxLB923ELHYMkHmlxiqFy4lw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],

View file

@ -147,6 +147,7 @@
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-svg-icons-ng": "^1.5.2", "vite-plugin-svg-icons-ng": "^1.5.2",
"vite-static-assets-plugin": "^1.2.2", "vite-static-assets-plugin": "^1.2.2",
"vite-tsconfig-paths": "^6.1.1" "vite-tsconfig-paths": "^6.1.1",
"zod-to-ts": "^2.0.0"
} }
} }

View file

@ -4,6 +4,11 @@ import sdkTsConfig from './sdk/sdk.tsconfig.json';
import sdkPackage from './sdk/package.json'; import sdkPackage from './sdk/package.json';
import { emptyDir } from 'fs-extra'; import { emptyDir } from 'fs-extra';
import { generateDtsBundle } from 'dts-bundle-generator'; import { generateDtsBundle } from 'dts-bundle-generator';
import { zodToTs, createAuxiliaryTypeStore, printNode } from 'zod-to-ts';
import * as types from './sdk/sdk';
const zodTypeRegex = /z\.infer<typeof? ([\w\d]+)>/gm;
async function generateApiDeclarations () async function generateApiDeclarations ()
{ {
@ -19,7 +24,29 @@ async function generateApiDeclarations ()
} }
},], { preferredConfigPath: './scripts/sdk/sdk.tsconfig.json' }); },], { preferredConfigPath: './scripts/sdk/sdk.tsconfig.json' });
await Bun.write('./dist-sdk/index.d.ts', results); const auxiliaryTypeStore = createAuxiliaryTypeStore();
await Bun.write('./dist-sdk/index.d.ts', results.map(r =>
{
const result = r;
return result.replaceAll(zodTypeRegex, (e, name) =>
{
const schema = types[name as keyof typeof types];
if (schema)
{
try
{
const { node } = zodToTs(schema as any, { auxiliaryTypeStore, unrepresentable: 'any' });
return printNode(node);
} catch (error)
{
console.error(error);
return e;
}
}
return e;
});
}));
const pkg = { const pkg = {
...sdkPackage, ...sdkPackage,
@ -29,7 +56,7 @@ async function generateApiDeclarations ()
author: appPkg.author, author: appPkg.author,
peerDependencies: appPkg.dependencies peerDependencies: appPkg.dependencies
}; };
await Bun.write(path.join(outDir, '..', 'package.json'), JSON.stringify(pkg, null, 3)); await Bun.write(path.join(outDir, 'package.json'), JSON.stringify(pkg, null, 3));
} }
await generateApiDeclarations(); await generateApiDeclarations();

14
scripts/sdk/README.md Normal file
View file

@ -0,0 +1,14 @@
# Gameflow Deck SDK
This is the type definitions for Gameflow Deck plugins.
## Developing a plugin
The plugin must have a default export class of type `PluginType`. It exposes the context and all the hooks to be tapped.
Gameflow uses the [Tapable Hooks](https://github.com/webpack/tapable).
The package must expose a main script gameflow will import and validate. It must implement the type fields on `PluginDescriptionType`.
## Publishing
For the plugin to show up in the UI for download. It must be published to NPM with the `gameflow-plugin` keyword. Gameflow uses bun to install plugins as packages from npmjs.

View file

@ -1,4 +1,9 @@
{ {
"name": "gameflow-sdk", "name": "gameflow-sdk",
"types": "index.d.ts" "types": "index.d.ts",
"description": "plugin SDK for the Gameflow Deck Launcher",
"keywords": [
"gameflow",
"sdk"
]
} }

View file

@ -14,7 +14,7 @@
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"declaration": true, "declaration": true,
"strict": true, "strict": true,
"outDir": "../../dist-sdk/sdk", "outDir": "../../dist-sdk",
"types": [ "types": [
"node" "node"
], ],
@ -38,10 +38,5 @@
"../../src/mainview/scripts/queries/*" "../../src/mainview/scripts/queries/*"
] ]
} }
}, }
"include": [
"../../src/bun/api/hooks",
"../../src/bun/types",
"../../src/shared"
]
} }

View file

@ -9,8 +9,8 @@ export const PluginContextSchema = z.object({
export const PluginLoadingContextSchema = z.object({ export const PluginLoadingContextSchema = z.object({
setProgress: z.function().input([z.number(), z.string()]).output(z.void()), setProgress: z.function().input([z.number(), z.string()]).output(z.void()),
config: z.instanceof(Conf), config: z.instanceof(Conf).describe("Per plugin config. It will use the settings schema defined in the plugin class"),
zodRegistry: z.instanceof($ZodRegistry) zodRegistry: z.instanceof($ZodRegistry).describe("Used by the settings to register metadata for the UI")
}).extend(PluginContextSchema.shape); }).extend(PluginContextSchema.shape);
export const PluginDescriptionSchema = z.object({ export const PluginDescriptionSchema = z.object({
@ -18,24 +18,24 @@ export const PluginDescriptionSchema = z.object({
displayName: z.string(), displayName: z.string(),
version: z.string(), version: z.string(),
description: z.string(), description: z.string(),
icon: z.url().optional(), icon: z.url().optional().describe("Can be an external URL to an image or a data url"),
keywords: z.array(z.string()).optional(), keywords: z.array(z.string()).optional(),
category: z.string().default("other"), category: z.string().default("other"),
main: z.string(), main: z.string().describe("The main entry. It must export a default class implementing PluginType"),
canDisable: z.boolean().default(true).optional() canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user")
}); });
export const PluginSchema = z.object({ export const PluginSchema = z.object({
load: z.function().input([PluginLoadingContextSchema]).output(z.promise(z.void())), load: z.function().input([PluginLoadingContextSchema]).output(z.promise(z.void())).describe("Called when the plugin is loaded or reloaded"),
cleanup: z.function().output(z.promise(z.void())).optional(), cleanup: z.function().output(z.promise(z.void())).optional().describe("Called when the plugin is unloaded or before it's reloaded"),
settingsSchema: z.instanceof(z.ZodObject).optional(), settingsSchema: z.instanceof(z.ZodObject).optional().describe("The settings schema. Gameflow will show settings in the UI."),
settingsMigrations: z.record(z.string(), z.function().input([z.instanceof(Conf)]).output(z.void())).optional(), settingsMigrations: z.record(z.string(), z.function().input([z.instanceof(Conf)]).output(z.void())).optional(),
eventsNames: z.object({ eventsNames: z.object({
id: z.string(), id: z.string(),
title: z.string().optional(), title: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
action: z.string() action: z.string()
}).array().optional(), }).array().optional().describe("Events will be called when the user presses the button in plugin settings. Each event creates a button."),
onEvent: z.function().input([z.string()]).output(z.object({ onEvent: z.function().input([z.string()]).output(z.object({
openTab: z.string().optional(), openTab: z.string().optional(),
reload: z.boolean().optional() reload: z.boolean().optional()