From b689233e91774253dd96263b5102284411124b10 Mon Sep 17 00:00:00 2001 From: lsy Date: Wed, 27 Nov 2024 01:02:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E9=85=8D=E7=BD=AE=E8=8E=B7=E5=8F=96=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E3=80=82=20=E5=89=8D=E7=AB=AF=EF=BC=9A=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E4=BA=86=E9=BB=98=E8=AE=A4=E7=8E=AF=E5=A2=83=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=80=BC=EF=BC=8C=E5=88=9B=E5=BB=BA=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E8=B7=AF=E7=94=B1=EF=BC=8C=E5=B0=86=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E5=92=8C=E6=9C=8D=E5=8A=A1=E5=90=88=E5=B9=B6=E5=88=B0=E6=A0=B8?= =?UTF-8?q?=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/settings.rs | 21 ++- frontend/app/env.d.ts | 8 +- frontend/app/init.tsx | 3 + frontend/app/root.tsx | 29 ++-- frontend/app/routes.tsx | 15 ++ frontend/app/routes/_index.tsx | 138 ------------------ frontend/common/serializableType.ts | 21 +++ frontend/contracts/capabilityContract.ts | 5 - frontend/contracts/generalContract.ts | 14 -- frontend/contracts/pluginContract.ts | 17 --- frontend/contracts/templateContract.ts | 11 -- frontend/contracts/themeContract.ts | 30 ---- .../{services/apiService.ts => core/api.ts} | 0 .../capability.ts} | 7 +- .../pluginService.ts => core/plugin.ts} | 26 +++- frontend/core/route.ts | 38 +++++ frontend/core/theme.ts | 98 +++++++++++++ frontend/hooks/servicesProvider.tsx | 16 +- frontend/public/favicon.ico | Bin 0 -> 10106 bytes frontend/services/routeManager.ts | 34 ----- frontend/services/themeService.ts | 123 ---------------- frontend/tailwind.config.ts | 2 +- frontend/vite.config.ts | 61 +++++--- src/hooks/useService.ts | 19 +++ src/types/config.ts | 13 ++ src/types/plugin.ts | 17 +++ 26 files changed, 329 insertions(+), 437 deletions(-) create mode 100644 frontend/app/init.tsx create mode 100644 frontend/app/routes.tsx delete mode 100644 frontend/app/routes/_index.tsx create mode 100644 frontend/common/serializableType.ts delete mode 100644 frontend/contracts/capabilityContract.ts delete mode 100644 frontend/contracts/generalContract.ts delete mode 100644 frontend/contracts/pluginContract.ts delete mode 100644 frontend/contracts/templateContract.ts delete mode 100644 frontend/contracts/themeContract.ts rename frontend/{services/apiService.ts => core/api.ts} (100%) rename frontend/{services/capabilityService.ts => core/capability.ts} (94%) rename frontend/{services/pluginService.ts => core/plugin.ts} (75%) create mode 100644 frontend/core/route.ts create mode 100644 frontend/core/theme.ts create mode 100644 frontend/public/favicon.ico delete mode 100644 frontend/services/routeManager.ts delete mode 100644 frontend/services/themeService.ts create mode 100644 src/hooks/useService.ts create mode 100644 src/types/config.ts create mode 100644 src/types/plugin.ts diff --git a/backend/src/api/settings.rs b/backend/src/api/settings.rs index 0b2081f..ba81eb7 100644 --- a/backend/src/api/settings.rs +++ b/backend/src/api/settings.rs @@ -17,7 +17,7 @@ use std::sync::Arc; pub struct SystemConfigure { pub author_name: String, pub current_theme: String, - pub site_keyword: Vec, + pub site_keyword: String, pub site_description: String, pub admin_path: String, } @@ -27,7 +27,7 @@ impl Default for SystemConfigure { Self { author_name: "lsy".to_string(), current_theme: "default".to_string(), - site_keyword: vec!["echoes".to_string()], + site_keyword: "echoes".to_string(), site_description: "echoes是一个高效、可扩展的博客平台".to_string(), admin_path: "admin".to_string(), } @@ -91,8 +91,21 @@ pub async fn system_config_get( _token: SystemToken, ) -> AppResult> { let sql = state.sql_get().await.into_app_result()?; - let configure = get_setting(&sql, "system".to_string(), "settings".to_string()) + let settings = get_setting(&sql, "system".to_string(), "settings".to_string()) .await .into_app_result()?; - Ok(configure) + Ok(settings) } + +#[get("/theme/")] +pub async fn theme_config_get( + state: &State>, + _token: SystemToken, + name: String, +) -> AppResult> { + let sql = state.sql_get().await.into_app_result()?; + let settings = get_setting(&sql, "theme".to_string(), name) + .await + .into_app_result()?; + Ok(settings) +} \ No newline at end of file diff --git a/frontend/app/env.d.ts b/frontend/app/env.d.ts index 71ad528..4ecb1e6 100644 --- a/frontend/app/env.d.ts +++ b/frontend/app/env.d.ts @@ -8,14 +8,10 @@ interface ImportMetaEnv { readonly VITE_SERVER_API: string; // 用于访问API的基础URL - readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径 - readonly VITE_CONTENT_PATH: string; //mark文章存储的位置 - readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置 - readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径 - readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径 + readonly VITE_SYSTEM_PORT: number; // 系统端口 VITE_SYSTEM_USERNAME: string; // 前端账号名称 VITE_SYSTEM_PASSWORD: string; // 前端账号密码 - VITE_SYSTEM_STATUS: boolean; // 系统是否进行安装 + VITE_INIT_STATUS: boolean; // 系统是否进行安装 } interface ImportMeta { diff --git a/frontend/app/init.tsx b/frontend/app/init.tsx new file mode 100644 index 0000000..d3a8ed4 --- /dev/null +++ b/frontend/app/init.tsx @@ -0,0 +1,3 @@ +export default function page(){ + return <>安装中 +} \ No newline at end of file diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 61c8b98..6006375 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -2,25 +2,13 @@ import { Links, Meta, Outlet, - Scripts, ScrollRestoration, } from "@remix-run/react"; -import type { LinksFunction } from "@remix-run/node"; -import "./tailwind.css"; +import { BaseProvider } from "hooks/servicesProvider"; +import { LinksFunction } from "@remix-run/react/dist/routeModules"; -export const links: LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; +import "~/tailwind.css"; export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -28,18 +16,23 @@ export function Layout({ children }: { children: React.ReactNode }) { + {children} - ); } - export default function App() { - return ; + return ( + + + + + + ); } diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx new file mode 100644 index 0000000..db56338 --- /dev/null +++ b/frontend/app/routes.tsx @@ -0,0 +1,15 @@ +import { useState } from "react"; + +import ReactDOMServer from 'react-dom/server'; +import { useLocation } from 'react-router-dom'; + +const MyComponent = () => { + return
Hello, World!
; +}; + + +export default function Routes() { + const htmlString = ReactDOMServer.renderToString(); + + return (
安装重构
) + } \ No newline at end of file diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx deleted file mode 100644 index 13a5c00..0000000 --- a/frontend/app/routes/_index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import type { MetaFunction } from "@remix-run/node"; - -export const meta: MetaFunction = () => { - return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, - ]; -}; - -export default function Index() { - return ( -
-
-
-

- Welcome to Remix -

-
- Remix - Remix -
-
- -
-
- ); -} - -const resources = [ - { - href: "https://remix.run/start/quickstart", - text: "Quick Start (5 min)", - icon: ( - - - - ), - }, - { - href: "https://remix.run/start/tutorial", - text: "Tutorial (30 min)", - icon: ( - - - - ), - }, - { - href: "https://remix.run/docs", - text: "Remix Docs", - icon: ( - - - - ), - }, - { - href: "https://rmx.as/discord", - text: "Join Discord", - icon: ( - - - - ), - }, -]; diff --git a/frontend/common/serializableType.ts b/frontend/common/serializableType.ts new file mode 100644 index 0000000..fd18142 --- /dev/null +++ b/frontend/common/serializableType.ts @@ -0,0 +1,21 @@ +export type Serializable = + | null + | number + | string + | boolean + | { [key: string]: Serializable } + | Array; +export interface Configuration { + [key: string]: { + title: string; + description?: string; + data: Serializable; + }; +} + +export interface PathDescription { + path: string; + name: string; + description?: string; +} + diff --git a/frontend/contracts/capabilityContract.ts b/frontend/contracts/capabilityContract.ts deleted file mode 100644 index b6c7930..0000000 --- a/frontend/contracts/capabilityContract.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface CapabilityProps { - name: string; - description?: string; - execute: (...args: any[]) => Promise; -} diff --git a/frontend/contracts/generalContract.ts b/frontend/contracts/generalContract.ts deleted file mode 100644 index 08b7f46..0000000 --- a/frontend/contracts/generalContract.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type SerializeType = - | null - | number - | string - | boolean - | { [key: string]: SerializeType } - | Array; -export interface Configuration { - [key: string]: { - title: string; - description?: string; - data: SerializeType; - }; -} diff --git a/frontend/contracts/pluginContract.ts b/frontend/contracts/pluginContract.ts deleted file mode 100644 index 1b8de80..0000000 --- a/frontend/contracts/pluginContract.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Configuration } from "contracts/generalContract"; - -export interface PluginConfig { - name: string; - version: string; - displayName: string; - description?: string; - author?: string; - enabled: boolean; - icon?: string; - managePath?: string; - configuration?: Configuration; - routs: Set<{ - description?: string; - path: string; - }>; -} diff --git a/frontend/contracts/templateContract.ts b/frontend/contracts/templateContract.ts deleted file mode 100644 index 675faba..0000000 --- a/frontend/contracts/templateContract.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface TemplateContract { - name: string; - description?: string; - config: { - layout?: string; - styles?: string[]; - scripts?: string[]; - }; - loader: () => Promise; - element: () => React.ReactNode; -} diff --git a/frontend/contracts/themeContract.ts b/frontend/contracts/themeContract.ts deleted file mode 100644 index 8378147..0000000 --- a/frontend/contracts/themeContract.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Configuration } from "contracts/generalContract"; -export interface ThemeConfig { - name: string; - displayName: string; - icon?: string; - version: string; - description?: string; - author?: string; - templates: Map; - globalSettings?: { - layout?: string; - css?: string; - }; - configuration: Configuration; - routes: { - index: string; - post: string; - tag: string; - category: string; - error: string; - loding: string; - page: Map; - }; -} - -export interface ThemeTemplate { - path: string; - name: string; - description?: string; -} diff --git a/frontend/services/apiService.ts b/frontend/core/api.ts similarity index 100% rename from frontend/services/apiService.ts rename to frontend/core/api.ts diff --git a/frontend/services/capabilityService.ts b/frontend/core/capability.ts similarity index 94% rename from frontend/services/capabilityService.ts rename to frontend/core/capability.ts index 841e2aa..b562107 100644 --- a/frontend/services/capabilityService.ts +++ b/frontend/core/capability.ts @@ -1,4 +1,9 @@ -import { CapabilityProps } from "contracts/capabilityContract"; +export interface CapabilityProps { + name: string; + description?: string; + execute: (...args: any[]) => Promise; +} + export class CapabilityService { private capabilities: Map< diff --git a/frontend/services/pluginService.ts b/frontend/core/plugin.ts similarity index 75% rename from frontend/services/pluginService.ts rename to frontend/core/plugin.ts index 7465e4c..3462a00 100644 --- a/frontend/services/pluginService.ts +++ b/frontend/core/plugin.ts @@ -1,10 +1,24 @@ -import { PluginConfiguration } from "types/pluginRequirement"; -import { Contracts } from "contracts/capabilityContract"; + +export interface PluginConfig { + name: string; + version: string; + displayName: string; + description?: string; + author?: string; + enabled: boolean; + icon?: string; + managePath?: string; + configuration?: Configuration; + routes: Set<{ + description?: string; + path: string; + }>; +} + + export class PluginManager { - private plugins: Map = new Map(); - private configurations: Map = new Map(); - private extensions: Map = new Map(); + private configurations: Map = new Map(); async loadPlugins() { const pluginDirs = await this.scanPluginDirectory(); @@ -37,7 +51,7 @@ export class PluginManager { async getPluginConfig( pluginName: string, - ): Promise { + ): Promise { const dbConfig = await this.fetchConfigFromDB(pluginName); if (dbConfig) { return dbConfig; diff --git a/frontend/core/route.ts b/frontend/core/route.ts new file mode 100644 index 0000000..87c952f --- /dev/null +++ b/frontend/core/route.ts @@ -0,0 +1,38 @@ +import { ReactNode } from "react"; // Import React +import { LoaderFunction } from "react-router-dom"; + + +interface RouteElement { + element: ReactNode, + loader?: LoaderFunction, + children?: RouteElement[], +} + +export class RouteManager { + private static instance: RouteManager; + private routes = new Map(); + private routesCache = new Map(); + + private constructor() { } + + public static getInstance(): RouteManager { + if (!RouteManager.instance) { + RouteManager.instance = new RouteManager(); + } + return RouteManager.instance; + } + + private createRouteElement( + path: string, + element: RouteElement + ) { + this.routes.set(path, element); + } + + private getRoutes(path: string): RouteElement | undefined { + return this.routes.get(path); + } + private getRoutesCache(path: string): string | undefined { + return this.routesCache.get(path); + } +} diff --git a/frontend/core/theme.ts b/frontend/core/theme.ts new file mode 100644 index 0000000..d6c50d4 --- /dev/null +++ b/frontend/core/theme.ts @@ -0,0 +1,98 @@ +import { Configuration, PathDescription } from "common/serializableType"; +import { ApiService } from "./api"; + +export interface ThemeConfig { + name: string; + displayName: string; + icon?: string; + version: string; + description?: string; + author?: string; + templates: Map; + globalSettings?: { + layout?: string; + css?: string; + }; + configuration: Configuration; + routes: { + index: string; + post: string; + tag: string; + category: string; + error: string; + loading: string; + page: Map; + }; +} + + + +export interface Template { + name: string; + description?: string; + config: { + layout?: string; + styles?: string[]; + scripts?: string[]; + }; + loader: () => Promise; + element: () => React.ReactNode; +} + + + +export class ThemeService { + private static instance: ThemeService; + private currentTheme?: ThemeConfig; + private api: ApiService; + + private constructor(api: ApiService) { + this.api = api; + } + + public static getInstance(api?: ApiService): ThemeService { + if (!ThemeService.instance && api) { + ThemeService.instance = new ThemeService(api); + } + return ThemeService.instance; + } + + public async getCurrentTheme(): Promise { + try { + const themeConfig = await this.api.request( + "/theme/current", + { method: "GET" }, + ); + this.currentTheme = themeConfig; + } catch (error) { + console.error("Failed to initialize theme:", error); + throw error; + } + } + + + public getThemeConfig(): ThemeConfig | undefined { + return this.currentTheme; + } + + + public async updateThemeConfig(config: Partial): Promise { + try { + const updatedConfig = await this.api.request( + "/theme/config", + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(config), + }, + ); + + await this.loadTheme(updatedConfig); + } catch (error) { + console.error("Failed to update theme configuration:", error); + throw error; + } + } +} diff --git a/frontend/hooks/servicesProvider.tsx b/frontend/hooks/servicesProvider.tsx index 4898b6f..62fc45e 100644 --- a/frontend/hooks/servicesProvider.tsx +++ b/frontend/hooks/servicesProvider.tsx @@ -1,6 +1,6 @@ -import { CapabilityService } from "services/capabilityService"; -import { ThemeService } from "services/themeService"; -import { ApiService } from "services/apiService"; +import { CapabilityService } from "core/capability"; +import { ApiService } from "core/api"; +import { RouteManager } from "core/route"; import { createServiceContext } from "hooks/createServiceContext"; import { ReactNode } from "react"; @@ -9,18 +9,18 @@ export const { CapabilityProvider, useCapability } = createServiceContext( () => CapabilityService.getInstance(), ); -export const { ThemeProvider, useTheme } = createServiceContext("Theme", () => - ThemeService.getInstance(), +export const { RouteProvider, useRoute } = createServiceContext("Route", () => + RouteManager.getInstance(), ); export const { ApiProvider, useApi } = createServiceContext("Api", () => - ThemeService.getInstance(), + ApiService.getInstance(), ); -export const ServiceProvider = ({ children }: { children: ReactNode }) => ( +export const BaseProvider = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..602e823552dee76ea7eadfd76dc267f7be12259f GIT binary patch literal 10106 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 { - try { - const themeConfig = await this.api.request( - "/theme/current", - { method: "GET" }, - ); - await this.loadTheme(themeConfig); - } catch (error) { - console.error("Failed to initialize theme:", error); - throw error; - } - } - - private async loadTheme(config: ThemeConfig): Promise { - try { - this.currentTheme = config; - await this.loadTemplates(); - } catch (error) { - console.error("Failed to load theme:", error); - throw error; - } - } - - private async loadTemplates(): Promise { - if (!this.currentTheme) { - throw new Error("No theme configuration loaded"); - } - - const loadTemplate = async (template: ThemeTemplate) => { - try { - const response = await fetch(template.path); - const templateContent = await response.text(); - this.templates.set(template.name, templateContent); - } catch (error) { - console.error(`Failed to load template ${template.name}:`, error); - throw error; - } - }; - - const loadPromises = Array.from(this.currentTheme.templates.values()).map( - (template) => loadTemplate(template), - ); - - await Promise.all(loadPromises); - } - - public getThemeConfig(): ThemeConfig | undefined { - return this.currentTheme; - } - - public getTemplate(templateName: string): string { - const template = this.templates.get(templateName); - if (!template) { - throw new Error(`Template ${templateName} not found`); - } - return template; - } - - public getTemplateByRoute(route: string): string { - if (!this.currentTheme) { - throw new Error("No theme configuration loaded"); - } - - let templateName: string | undefined; - - if (route === "/") { - templateName = this.currentTheme.routes.index; - } else if (route.startsWith("/post/")) { - templateName = this.currentTheme.routes.post; - } else if (route.startsWith("/tag/")) { - templateName = this.currentTheme.routes.tag; - } else if (route.startsWith("/category/")) { - templateName = this.currentTheme.routes.category; - } else { - templateName = this.currentTheme.routes.page.get(route); - } - - if (!templateName) { - templateName = this.currentTheme.routes.error; - } - - return this.getTemplate(templateName); - } - - public async updateThemeConfig(config: Partial): Promise { - try { - const updatedConfig = await this.api.request( - "/theme/config", - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(config), - }, - ); - - await this.loadTheme(updatedConfig); - } catch (error) { - console.error("Failed to update theme configuration:", error); - throw error; - } - } -} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 5f06ad4..4c5c2ef 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,7 +1,7 @@ import type { Config } from "tailwindcss"; export default { - content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], + content: ["./app/**/*.{js,jsx,ts,tsx}"], theme: { extend: { fontFamily: { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e4e8cef..c529d23 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,24 +1,43 @@ import { vitePlugin as remix } from "@remix-run/dev"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; +import Routes from "~/routes" -declare module "@remix-run/node" { - interface Future { - v3_singleFetch: true; - } -} - -export default defineConfig({ - plugins: [ - remix({ - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_singleFetch: true, - v3_lazyRouteDiscovery: true, - }, - }), - tsconfigPaths(), - ], -}); +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + return { + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + v3_lazyRouteDiscovery: true, + }, + routes: (defineRoutes) => { + return defineRoutes((route) => { + if (!env.VITE_INIT_STATUS) { + route("/", "init.tsx", { id: "index-route" }); + route("*", "init.tsx", { id: "catch-all-route" }); + } + else { + route("/", "routes.tsx", { id: "index-route" }); + route("*", "routes.tsx", { id: "catch-all-route" }); + } + }); + } + }), + tsconfigPaths(), + ], + define: { + "import.meta.env.VITE_SYSTEM_STATUS": JSON.stringify(false), + "import.meta.env.VITE_SERVER_API": JSON.stringify("localhost:22000"), + "import.meta.env.VITE_SYSTEM_PORT": JSON.stringify(22100), + }, + server: { + port: Number(env.VITE_SYSTEM_PORT ?? 22100), + strictPort: true, + }, + }; +}); \ No newline at end of file diff --git a/src/hooks/useService.ts b/src/hooks/useService.ts new file mode 100644 index 0000000..76a0603 --- /dev/null +++ b/src/hooks/useService.ts @@ -0,0 +1,19 @@ +export function createServiceHook(name: string, getInstance: () => T) { + const Context = createContext(null); + + const Provider: FC = ({ children }) => ( + + {children} + + ); + + const useService = () => { + const service = useContext(Context); + if (!service) { + throw new Error(`use${name} must be used within ${name}Provider`); + } + return service; + }; + + return [Provider, useService] as const; +} \ No newline at end of file diff --git a/src/types/config.ts b/src/types/config.ts new file mode 100644 index 0000000..a21d158 --- /dev/null +++ b/src/types/config.ts @@ -0,0 +1,13 @@ +export type Json = + | null + | number + | string + | boolean + | { [key: string]: Json } + | Json[]; + +export type Config = Record; \ No newline at end of file diff --git a/src/types/plugin.ts b/src/types/plugin.ts new file mode 100644 index 0000000..c36fc09 --- /dev/null +++ b/src/types/plugin.ts @@ -0,0 +1,17 @@ +export interface PluginDefinition { + meta: { + name: string; + version: string; + displayName: string; + description?: string; + author?: string; + icon?: string; + }; + config?: Config; + routes: { + path: string; + description?: string; + }[]; + enabled: boolean; + managePath?: string; +} \ No newline at end of file