Plugin Development Guide

Create Your Own Plugins

Learn how to create Hype plugins from scratch with practical examples and best practices. Build reusable components that extend Hype's capabilities.

๐Ÿ“‹ Prerequisites

  • โœ… Basic Lua knowledge
  • โœ… Hype installed and working
  • โœ… Text editor

๐ŸŽฏ Creating Your First Plugin

Step 1: Plugin Setup

Create a new directory for your plugin:

mkdir my-first-plugin
cd my-first-plugin

Step 2: Create the Manifest

Create hype-plugin.yaml:

name: "my-first-plugin"
version: "1.0.0"
type: "lua"
main: "plugin.lua"
description: "My first Hype plugin"
author: "Your Name"
license: "MIT"

Step 3: Create the Plugin Code

Create plugin.lua:

-- My first Hype plugin
local myfirst = {}

-- Simple greeting function
function myfirst.greet(name)
    return "Hello, " .. (name or "World") .. " from my first plugin!"
end

-- Function with error handling
function myfirst.divide(a, b)
    -- Validate inputs
    if type(a) ~= "number" or type(b) ~= "number" then
        return nil, "Both arguments must be numbers"
    end
    
    if b == 0 then
        return nil, "Division by zero"
    end
    
    return a / b, nil
end

-- Plugin metadata
myfirst._VERSION = "1.0.0"
myfirst._DESCRIPTION = "My first Hype plugin"

-- Must return the plugin table
return myfirst

Step 4: Test the Plugin

Create a test script test.lua:

-- Test script for my-first-plugin
local myfirst = require("my-first-plugin")

-- Test greeting
print(myfirst.greet("Developer"))

-- Test division
local result, err = myfirst.divide(10, 2)
if result then
    print("10 / 2 =", result)
else
    print("Error:", err)
end

-- Test error handling
local result, err = myfirst.divide(10, 0)
if result then
    print("Result:", result)
else
    print("Expected error:", err)
end

Run the test:

./hype run test.lua --plugins ./my-first-plugin

๐Ÿ”ง Development Patterns

Error Handling Pattern

function myplugin.operation(input)
    -- Validate input
    if not input then
        return nil, "Input is required"
    end
    
    if type(input) ~= "string" then
        return nil, "Input must be a string"
    end
    
    -- Perform operation
    local result = processInput(input)
    
    -- Check for errors
    if not result then
        return nil, "Processing failed"
    end
    
    return result, nil
end

Optional Parameters Pattern

function myplugin.format(text, options)
    -- Default options
    options = options or {}
    
    local prefix = options.prefix or ""
    local suffix = options.suffix or ""
    local uppercase = options.uppercase or false
    
    local result = prefix .. text .. suffix
    
    if uppercase then
        result = result:upper()
    end
    
    return result
end

State Management Pattern

local myplugin = {}

-- Private state
local cache = {}
local initialized = false

-- Private functions
local function init()
    if initialized then return end
    
    -- Initialize plugin state
    cache = {}
    initialized = true
end

-- Public functions
function myplugin.set(key, value)
    init()
    cache[key] = value
    return true
end

function myplugin.get(key)
    init()
    return cache[key]
end

return myplugin

๐ŸŒŸ Real-World Examples

๐Ÿ“ File Watcher Plugin

Monitor file changes and detect modifications

local watcher = {}

-- Store watched files and their last modification times
local watched = {}

function watcher.watch(filepath)
    local file = io.open(filepath, "r")
    if not file then
        return false, "File not found: " .. filepath
    end
    file:close()
    
    -- Get file modification time
    local handle = io.popen("stat -c %Y " .. filepath .. " 2>/dev/null")
    local mtime = handle:read("*a")
    handle:close()
    
    watched[filepath] = tonumber(mtime) or 0
    return true, nil
end

function watcher.checkChanges()
    local changes = {}
    
    for filepath, oldtime in pairs(watched) do
        local handle = io.popen("stat -c %Y " .. filepath .. " 2>/dev/null")
        local newtime = tonumber(handle:read("*a")) or 0
        handle:close()
        
        if newtime > oldtime then
            table.insert(changes, filepath)
            watched[filepath] = newtime
        end
    end
    
    return changes
end

watcher._VERSION = "1.0.0"
return watcher

โš™๏ธ Configuration Plugin

Parse and manage INI-style configuration files

local config = {}

-- Private state
local settings = {}

-- Parse simple INI-style config
local function parseINI(content)
    local result = {}
    local section = "default"
    
    for line in content:gmatch("[^\r\n]+") do
        line = line:match("^%s*(.-)%s*$") -- trim whitespace
        
        if line:match("^%[(.+)%]$") then
            -- Section header
            section = line:match("^%[(.+)%]$")
            result[section] = result[section] or {}
        elseif line:match("^([^=]+)=(.*)$") then
            -- Key=value pair
            local key, value = line:match("^([^=]+)=(.*)$")
            key = key:match("^%s*(.-)%s*$")
            value = value:match("^%s*(.-)%s*$")
            
            result[section] = result[section] or {}
            result[section][key] = value
        end
    end
    
    return result
end

function config.load(filepath)
    local file = io.open(filepath, "r")
    if not file then
        return false, "Could not read config file: " .. filepath
    end
    
    local content = file:read("*a")
    file:close()
    
    settings = parseINI(content)
    return true, nil
end

function config.get(section, key, default)
    if not settings[section] then
        return default
    end
    
    local value = settings[section][key]
    if value == nil then
        return default
    end
    
    -- Try to convert to number or boolean
    if value == "true" then
        return true
    elseif value == "false" then
        return false
    elseif tonumber(value) then
        return tonumber(value)
    else
        return value
    end
end

config._VERSION = "1.0.0"
return config

๐Ÿ“ Template Engine Plugin

Simple template engine with variable substitution

local template = {}

-- Simple template engine with {{variable}} syntax
function template.render(templateStr, variables)
    if type(templateStr) ~= "string" then
        return nil, "Template must be a string"
    end
    
    if type(variables) ~= "table" then
        return nil, "Variables must be a table"
    end
    
    local result = templateStr
    
    -- Replace {{variable}} with values
    result = result:gsub("{{%s*([%w_]+)%s*}}", function(varname)
        local value = variables[varname]
        if value ~= nil then
            return tostring(value)
        else
            return "{{" .. varname .. "}}" -- leave unchanged
        end
    end)
    
    return result, nil
end

function template.renderFile(templatePath, variables, outputPath)
    -- Read template
    local file = io.open(templatePath, "r")
    if not file then
        return false, "Could not read template: " .. templatePath
    end
    
    local templateStr = file:read("*a")
    file:close()
    
    -- Render template
    local result, err = template.render(templateStr, variables)
    if not result then
        return false, "Template rendering failed: " .. err
    end
    
    -- Write output if specified
    if outputPath then
        local outFile = io.open(outputPath, "w")
        if not outFile then
            return false, "Could not write output: " .. outputPath
        end
        
        outFile:write(result)
        outFile:close()
    end
    
    return result, nil
end

template._VERSION = "1.0.0"
return template

๐Ÿงช Testing Your Plugins

Unit Testing Approach

Create a simple test framework for your plugins:

-- tests/test_plugin.lua
local tests = {}
local passed = 0
local failed = 0

function tests.assert(condition, message)
    if condition then
        passed = passed + 1
        print("โœ“ " .. (message or "Test passed"))
    else
        failed = failed + 1
        print("โœ— " .. (message or "Test failed"))
    end
end

function tests.assertEqual(actual, expected, message)
    local condition = actual == expected
    if not condition then
        message = (message or "Values not equal") .. 
                 " (expected: " .. tostring(expected) .. 
                 ", actual: " .. tostring(actual) .. ")"
    end
    tests.assert(condition, message)
end

function tests.finish()
    print("\nTest Results:")
    print("Passed: " .. passed)
    print("Failed: " .. failed)
    print("Total: " .. (passed + failed))
    
    if failed > 0 then
        os.exit(1)
    end
end

-- Test the plugin
local myplugin = require("my-plugin")

-- Test greeting
local result = myplugin.greet("Test")
tests.assertEqual(result, "Hello, Test from my first plugin!", "Greeting test")

-- Test division success
local result, err = myplugin.divide(10, 2)
tests.assertEqual(result, 5, "Division success")
tests.assertEqual(err, nil, "No error on success")

-- Test division by zero
local result, err = myplugin.divide(10, 0)
tests.assertEqual(result, nil, "Division by zero returns nil")
tests.assert(err ~= nil, "Division by zero returns error")

tests.finish()

Run tests:

./hype run tests/test_plugin.lua --plugins ./my-plugin

๐Ÿš€ Advanced Development

Version Evolution

Maintain backward compatibility when updating plugins:

-- plugin.lua v2.0.0
local myplugin = {}

-- New function in v2.0.0
function myplugin.newFeature()
    return "This is new in v2.0.0"
end

-- Existing function (maintain compatibility)
function myplugin.oldFunction(arg)
    -- v2.0.0: enhanced but still compatible
    return "Enhanced: " .. (arg or "default")
end

-- Version-specific behavior
function myplugin.adaptiveFunction()
    -- Check if we're in a newer version context
    if myplugin._VERSION >= "2.0.0" then
        return myplugin.newFeature()
    else
        return "Fallback behavior"
    end
end

myplugin._VERSION = "2.0.0"
return myplugin

Performance Optimization

Optimize your plugins for better performance:

-- Optimize for performance
local myplugin = {}

-- Cache expensive computations
local cache = {}

-- Pre-compile patterns
local emailPattern = "[%w%._%+%-]+@[%w%._%+%-]+%.%w+"

function myplugin.validateEmail(email)
    -- Use cached result if available
    if cache[email] ~= nil then
        return cache[email]
    end
    
    local isValid = email:match(emailPattern) ~= nil
    cache[email] = isValid
    
    return isValid
end

-- Limit cache size to prevent memory leaks
local function limitCache()
    local count = 0
    for _ in pairs(cache) do
        count = count + 1
        if count > 1000 then
            cache = {} -- Clear cache if too large
            break
        end
    end
end

-- Clean up periodically
function myplugin.cleanup()
    limitCache()
    return true
end

return myplugin

๐Ÿ“ฆ Distribution and Sharing

Plugin Checklist

Before releasing your plugin:

  • โ˜ Plugin manifest is complete and valid
  • โ˜ Plugin code follows Lua conventions
  • โ˜ Error handling is comprehensive
  • โ˜ Functions are documented
  • โ˜ Version metadata is included
  • โ˜ Tests are written and passing
  • โ˜ README documentation exists
  • โ˜ Examples are provided
  • โ˜ License is specified

Versioning Strategy

Follow semantic versioning:

Major (1.0.0 โ†’ 2.0.0)
Breaking changes
Minor (1.0.0 โ†’ 1.1.0)
New features, backward compatible
Patch (1.0.0 โ†’ 1.0.1)
Bug fixes, backward compatible

๐ŸŽฏ Next Steps

  1. Study existing plugins in examples/plugins/
  2. Create your own plugin following this guide
  3. Share with the community by creating a repository
  4. Contribute improvements to the plugin system