npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gameap/plugin-sdk

v0.3.2

Published

SDK for developing GameAP frontend plugins

Readme

GameAP Plugin SDK

SDK for developing GameAP frontend plugins. This package provides types, utilities, and build configuration for creating Vue-based plugins that integrate with the GameAP.

Installation

npm install @gameap/plugin-sdk

Quick Start

  1. Copy the template from templates/plugin-template/ to your plugin directory
  2. Update package.json with your plugin details
  3. Implement your plugin in src/index.ts
  4. Build with npm run build

Plugin Structure

A plugin must export a PluginDefinition object:

import type { PluginDefinition } from '@gameap/plugin-sdk';
import MyComponent from './components/MyComponent.vue';

export const myPlugin: PluginDefinition = {
    id: 'my-plugin',
    name: 'My Plugin',
    version: '1.0.0',
    apiVersion: '1.0',
    description: 'A sample plugin',
    author: 'Your Name',
    routes: [...],
    menuItems: [...],
    slots: {...},
    homeButtons: [...],
    fileEditors: [...],
};

Plugin Definition Fields

| Field | Type | Required | Description | |-------|------|----------|-------------| | id | string | Yes | Unique plugin identifier (e.g., 'my-plugin') | | name | string | Yes | Human-readable plugin name | | version | string | Yes | Semantic version (e.g., '1.0.0') | | apiVersion | '1.0' | Yes | GameAP plugin API version | | description | string | No | Plugin description | | author | string | No | Plugin author | | routes | PluginRoute[] | No | Plugin routes to register | | menuItems | PluginMenuItem[] | No | Sidebar menu items | | slots | Record<SlotName, PluginSlotComponent[]> | No | Slot components | | homeButtons | PluginHomeButton[] | No | Home page buttons | | fileEditors | PluginFileEditor[] | No | Custom file editors for file manager | | translations | Record<string, Record<string, string>> | No | Plugin translations by language | | onInit | () => void \| Promise<void> | No | Initialization hook | | onDestroy | () => void \| Promise<void> | No | Cleanup hook |

Routes

Plugin routes are registered under /plugins/{pluginId}/:

routes: [
    {
        path: '/',              // Resolves to /plugins/my-plugin/
        name: 'index',          // Route name: plugin.my-plugin.index
        component: MyPage,
        meta: {
            title: 'My Page',
            requiresAuth: true,
            requiresAdmin: false,
        },
        children: [...],        // Nested routes
    },
],

Menu Items

Add items to the sidebar:

menuItems: [
    {
        section: 'servers',     // 'servers' | 'admin' | 'custom'
        icon: 'fas fa-puzzle-piece',
        text: 'My Plugin',
        route: { name: 'index' },
        order: 100,
        adminOnly: false,
    },
],

Slots

Inject components into predefined slots:

slots: {
    'server-tabs': [
        {
            component: ServerTab,
            order: 100,
            label: 'My Tab',
            icon: 'fas fa-puzzle-piece',
            name: 'my-tab',
        },
    ],
    'dashboard-widgets': [
        {
            component: DashboardWidget,
            order: 50,
            label: 'My Widget',
        },
    ],
},

Available Slots

| Slot Name | Description | Props | |-----------|-------------|-------| | server-tabs | Tabs on server detail pages | ServerTabProps | | dashboard-widgets | Widgets on the dashboard | DashboardWidgetProps | | sidebar-sections | Sections in the sidebar | - | | admin-pages | Admin page components | - |

Home Buttons

Add buttons to the home page:

homeButtons: [
    {
        name: 'My Plugin',
        icon: 'fas fa-puzzle-piece',
        route: { name: 'index' },
        order: 100,
    },
],

File Editors

Plugins can register custom file editors for the file manager. Editors provide specialized UIs for editing specific file types.

import type { PluginDefinition } from '@gameap/plugin-sdk';
import ServerCfgEditor from './components/ServerCfgEditor.vue';
import IniEditor from './components/IniEditor.vue';
import HexEditor from './components/HexEditor.vue';

export const myPlugin: PluginDefinition = {
    id: 'my-plugin',
    name: 'My Plugin',
    version: '1.0.0',
    apiVersion: '1.0',
    fileEditors: [
        {
            id: 'server-cfg',
            name: 'Server Config Editor',
            component: ServerCfgEditor,
            match: {
                fileName: 'server.cfg',
                gameCode: 'cstrike'
            },
            icon: 'fa-solid fa-gear'
        },
        {
            id: 'ini-editor',
            name: 'INI Editor',
            component: IniEditor,
            match: {
                extensions: ['ini', 'cfg']
            }
        },
        {
            id: 'hex-editor',
            name: 'Hex Editor',
            component: HexEditor,
            match: {
                allFiles: true
            },
            contentType: 'binary',
            icon: 'fa-solid fa-memory'
        }
    ]
};

File Editor Definition

| Field | Type | Required | Description | |-------|------|----------|-------------| | id | string | Yes | Unique identifier within the plugin | | name | string | Yes | Display name shown in context menu | | component | Component | Yes | Vue component that renders the editor | | match | EditorMatchRules | Yes | Rules for when this editor is available | | contentType | 'text' \| 'binary' | No | Content type (default: 'text') | | readOnly | boolean | No | If true, editor is view-only (no save) | | icon | string | No | Font Awesome icon class for context menu |

Matching Rules

Editors match files based on these rules (all specified rules must match):

| Rule | Description | Example | |------|-------------|---------| | allFiles | Match all files (lowest specificity) | true | | fileName | Exact file name match | 'server.cfg' | | pathContains | File path must contain string | 'amxmodx/configs/' | | fullPath | Exact full path match | '/cstrike/server.cfg' | | extensions | Array of file extensions | ['ini', 'cfg', 'json'] | | fileNameRegexp | Regex pattern for file name | '^server.*\\.cfg$' | | gameCode | Match only for this game code | 'cstrike' | | gameName | Match only for this game name | 'Counter-Strike' |

Specificity and Default Editor

When multiple editors match a file:

  • All matching editors are shown in the context menu
  • The most specific editor is marked as "(default)" and opens on double-click
  • Specificity order: fullPath > pathContains > fileName > fileNameRegexp > extensions > allFiles
  • Game filters (gameCode/gameName) add to specificity score
  • allFiles: true has the lowest specificity (score=1), useful for generic editors like hex viewers

Editor Component Props

Editor components receive these props:

interface FileEditorProps {
    content: string | ArrayBuffer;  // File content
    filePath: string;               // Full file path
    fileName: string;               // File name with extension
    extension: string;              // File extension (without dot)
    gameCode?: string;              // Current server's game code
    gameName?: string;              // Current server's game name
    pluginId: string;               // Plugin ID
}

Editor Component Events

Emit these events from your editor component:

| Event | Payload | Description | |-------|---------|-------------| | save | string \| ArrayBuffer | Save the new content | | close | - | Close the editor without saving |

Example Editor Component

<template>
    <div class="my-editor">
        <textarea v-model="editedContent" rows="20" class="w-full"></textarea>
    </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
    content: { type: String, required: true },
    filePath: { type: String, required: true },
    fileName: { type: String, required: true },
    extension: { type: String, required: true },
    gameCode: { type: String, default: null },
    gameName: { type: String, default: null },
    pluginId: { type: String, required: true }
});

const emit = defineEmits(['save', 'close']);

const editedContent = ref('');

onMounted(() => {
    editedContent.value = props.content;
});

function save() {
    emit('save', editedContent.value);
}

function close() {
    emit('close');
}

defineExpose({ save, close });
</script>

Binary Content

For binary files (images, custom formats), set contentType: 'binary':

fileEditors: [
    {
        id: 'image-editor',
        name: 'Image Editor',
        component: ImageEditor,
        match: { extensions: ['png', 'jpg'] },
        contentType: 'binary'
    }
]

The content prop will be an ArrayBuffer instead of a string.

Internationalization (i18n)

Plugins can provide translations for multiple languages:

export const myPlugin: PluginDefinition = {
    id: 'my-plugin',
    name: 'My Plugin',
    version: '1.0.0',
    apiVersion: '1.0',
    translations: {
        en: {
            'greeting': 'Hello, :name!',
            'status.active': 'Active',
            'status.inactive': 'Inactive'
        },
        ru: {
            'greeting': 'Привет, :name!',
            'status.active': 'Активен',
            'status.inactive': 'Неактивен'
        }
    },
    routes: [...]
};

Using Translations in Plugin Routes

For components rendered as plugin routes (pages), use usePluginTrans:

<script setup>
import { usePluginTrans } from '@gameap/plugin-sdk';

const { trans, locale } = usePluginTrans();
</script>

<template>
    <p>{{ trans('greeting', { name: 'World' }) }}</p>
    <span>{{ trans('status.active') }}</span>
    <p>Current locale: {{ locale }}</p>
</template>

Using Translations in Slot Components

For slot components (server tabs, dashboard widgets, etc.), use providePluginTrans with the pluginId prop. This creates a translation context for the component and all its children:

<script setup>
import { providePluginTrans } from '@gameap/plugin-sdk';
import type { ServerTabProps } from '@gameap/plugin-sdk';

const props = defineProps<ServerTabProps>();

// providePluginTrans creates context for this component and its children
const { trans } = providePluginTrans(props.pluginId);
</script>

<template>
    <p>{{ trans('greeting', { name: 'World' }) }}</p>
</template>

Why two different functions?

  • usePluginTrans() - Use in plugin routes/pages where the translation context is automatically provided
  • providePluginTrans(pluginId) - Use in slot components (tabs, widgets) where you need to create the context manually

Child components of a slot component can still use usePluginTrans() - they will inherit the context from the parent.

The trans function:

  • Returns the translated string for the current locale
  • Falls back to English (en) if the current locale is not available
  • Returns the key itself if no translation is found
  • Supports parameter substitution using :paramName syntax

Translating Menu Items and Slot Labels

You can use the @:key syntax to reference translations in menu items, slots, and home buttons:

export const myPlugin: PluginDefinition = {
    translations: {
        en: {
            'menu_item': 'My Plugin',
            'server_tab': 'My Tab',
            'home_button': 'My Plugin'
        },
        ru: {
            'menu_item': 'Мой плагин',
            'server_tab': 'Моя вкладка',
            'home_button': 'Мой плагин'
        }
    },
    menuItems: [{
        text: '@:menu_item',  // Will be translated
        // ...
    }],
    slots: {
        'server-tabs': [{
            label: '@:server_tab',  // Will be translated
            // ...
        }]
    },
    homeButtons: [{
        name: '@:home_button',  // Will be translated
        // ...
    }]
};

The @:key syntax:

  • Prefix any text value with @: followed by the translation key
  • The value will be automatically resolved to the current locale's translation
  • Falls back to English if the current locale is not available
  • Returns the key itself if no translation is found

Context Hooks

Access GameAP context in your components:

<script setup>
import {
    usePluginContext,
    useServer,
    useServerId,
    useServerAbilities,
    useCurrentUser,
    useIsAdmin,
    useIsAuthenticated,
    usePluginRoute,
    usePluginId,
} from '@gameap/plugin-sdk';

// Full context access
const ctx = usePluginContext();

// Server data (on server pages)
const server = useServer();           // ServerData | null
const serverId = useServerId();       // number | null
const abilities = useServerAbilities(); // string[]

// User data
const user = useCurrentUser();        // UserData
const isAdmin = useIsAdmin();         // boolean
const isAuthenticated = useIsAuthenticated(); // boolean

// Route info
const route = usePluginRoute();       // PluginRouteInfo
const pluginId = usePluginId();       // string | null
</script>

Component Props

ServerTabProps

Passed to components in the server-tabs slot:

interface ServerTabProps {
    serverId: number;
    server: ServerData;
    pluginId: string;
}

DashboardWidgetProps

Passed to components in the dashboard-widgets slot:

interface DashboardWidgetProps {
    isAdmin: boolean;
    pluginId: string;
}

Data Types

ServerData

interface ServerData {
    id: number;
    uuid: string;
    name: string;
    game_id: string;
    game_mod_id: number;
    ip: string;
    port: number;
    query_port: number;
    rcon_port: number;
    enabled: boolean;
    installed: boolean;
    blocked: boolean;
    start_command: string;
    dir: string;
    process_active: boolean;
    last_process_check: string;
}

UserData

interface UserData {
    id: number;
    login: string;
    name: string;
    roles: string[];
    isAdmin: boolean;
    isAuthenticated: boolean;
}

Build Configuration

Use the provided Vite configuration helper:

// vite.config.js
import { createPluginConfig } from '@gameap/plugin-sdk/vite';

export default createPluginConfig({
    entry: 'src/index.ts',
    name: 'plugin',
    outDir: 'dist',
});

Integration with WASM Plugins

Frontend bundles are embedded in WASM plugins and served by the GameAP backend. The plugin manager calls GetFrontendBundle during plugin initialization to retrieve the compiled JavaScript bundle and CSS styles.

CSS Styles

Plugins can provide CSS styles that will be automatically loaded by GameAP. The CSS is returned alongside the JavaScript bundle in the GetFrontendBundle response.

When implementing your WASM plugin's GetFrontendBundle method, return styles in the response:

message GetFrontendBundleResponse {
  bytes bundle = 1;      // JavaScript bundle
  bool has_bundle = 2;   // Whether plugin has JS bundle
  bytes styles = 3;      // CSS styles
  bool has_styles = 4;   // Whether plugin has CSS styles
}

CSS from all plugins is combined and served at /plugins.css, which is automatically loaded before plugin JavaScript.

Development

# Watch mode for development
npm run dev

# Production build
npm run build

Template

A complete plugin template is available in templates/plugin-template/. Copy this directory as a starting point for your plugin.