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