diff --git a/bun.lock b/bun.lock index 217584d..3568727 100644 --- a/bun.lock +++ b/bun.lock @@ -101,6 +101,7 @@ "vite-plugin-svg-icons-ng": "^1.5.2", "vite-static-assets-plugin": "^1.2.2", "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-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=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], diff --git a/package.json b/package.json index 43264be..f83f902 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "vite": "^7.3.1", "vite-plugin-svg-icons-ng": "^1.5.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" } } \ No newline at end of file diff --git a/scripts/build-sdk.ts b/scripts/build-sdk.ts index 173b2fc..7018414 100644 --- a/scripts/build-sdk.ts +++ b/scripts/build-sdk.ts @@ -4,6 +4,11 @@ import sdkTsConfig from './sdk/sdk.tsconfig.json'; import sdkPackage from './sdk/package.json'; import { emptyDir } from 'fs-extra'; import { generateDtsBundle } from 'dts-bundle-generator'; +import { zodToTs, createAuxiliaryTypeStore, printNode } from 'zod-to-ts'; + +import * as types from './sdk/sdk'; + +const zodTypeRegex = /z\.infer/gm; async function generateApiDeclarations () { @@ -19,7 +24,29 @@ async function generateApiDeclarations () } },], { 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 = { ...sdkPackage, @@ -29,7 +56,7 @@ async function generateApiDeclarations () author: appPkg.author, 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(); \ No newline at end of file diff --git a/scripts/sdk/README.md b/scripts/sdk/README.md new file mode 100644 index 0000000..cfe9b61 --- /dev/null +++ b/scripts/sdk/README.md @@ -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. diff --git a/scripts/sdk/package.json b/scripts/sdk/package.json index 8d1b9eb..d92b663 100644 --- a/scripts/sdk/package.json +++ b/scripts/sdk/package.json @@ -1,4 +1,9 @@ { "name": "gameflow-sdk", - "types": "index.d.ts" + "types": "index.d.ts", + "description": "plugin SDK for the Gameflow Deck Launcher", + "keywords": [ + "gameflow", + "sdk" + ] } \ No newline at end of file diff --git a/scripts/sdk/sdk.tsconfig.json b/scripts/sdk/sdk.tsconfig.json index 20df404..8da436e 100644 --- a/scripts/sdk/sdk.tsconfig.json +++ b/scripts/sdk/sdk.tsconfig.json @@ -14,7 +14,7 @@ "emitDeclarationOnly": true, "declaration": true, "strict": true, - "outDir": "../../dist-sdk/sdk", + "outDir": "../../dist-sdk", "types": [ "node" ], @@ -38,10 +38,5 @@ "../../src/mainview/scripts/queries/*" ] } - }, - "include": [ - "../../src/bun/api/hooks", - "../../src/bun/types", - "../../src/shared" - ] + } } \ No newline at end of file diff --git a/src/bun/types/types.schema.ts b/src/bun/types/types.schema.ts index f56dd15..c4738fc 100644 --- a/src/bun/types/types.schema.ts +++ b/src/bun/types/types.schema.ts @@ -9,8 +9,8 @@ export const PluginContextSchema = z.object({ export const PluginLoadingContextSchema = z.object({ setProgress: z.function().input([z.number(), z.string()]).output(z.void()), - config: z.instanceof(Conf), - zodRegistry: z.instanceof($ZodRegistry) + config: z.instanceof(Conf).describe("Per plugin config. It will use the settings schema defined in the plugin class"), + zodRegistry: z.instanceof($ZodRegistry).describe("Used by the settings to register metadata for the UI") }).extend(PluginContextSchema.shape); export const PluginDescriptionSchema = z.object({ @@ -18,24 +18,24 @@ export const PluginDescriptionSchema = z.object({ displayName: z.string(), version: 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(), category: z.string().default("other"), - main: z.string(), - canDisable: z.boolean().default(true).optional() + main: z.string().describe("The main entry. It must export a default class implementing PluginType"), + canDisable: z.boolean().default(true).optional().describe("Can the plugin be disabled or enabled by the user") }); export const PluginSchema = z.object({ - load: z.function().input([PluginLoadingContextSchema]).output(z.promise(z.void())), - cleanup: z.function().output(z.promise(z.void())).optional(), - settingsSchema: z.instanceof(z.ZodObject).optional(), + 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().describe("Called when the plugin is unloaded or before it's reloaded"), + 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(), eventsNames: z.object({ id: z.string(), title: z.string().optional(), description: z.string().optional(), 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({ openTab: z.string().optional(), reload: z.boolean().optional()