Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lua Plugins

RSConstruct supports custom processors written in Lua. Drop a .lua file in the plugins/ directory and add a [processor.NAME] section in rsconstruct.toml. The plugin participates in discovery, execution, caching, cleaning, tool listing, and auto-detection just like a built-in processor.

Quick Start

1. Create the plugin file:

plugins/eslint.lua
function description()
    return "Lint JavaScript/TypeScript with ESLint"
end

function required_tools()
    return {"eslint"}
end

function discover(project_root, config, files)
    local products = {}
    for _, file in ipairs(files) do
        local stub = rsconstruct.stub_path(project_root, file, "eslint")
        table.insert(products, {
            inputs = {file},
            outputs = {stub},
        })
    end
    return products
end

function execute(product)
    rsconstruct.run_command("eslint", {product.inputs[1]})
    rsconstruct.write_stub(product.outputs[1], "linted")
end

2. Enable it in rsconstruct.toml:

[processor.eslint]
scan_dir = "src"
extensions = [".js", ".ts"]

3. Run it:

rsconstruct build            # builds including the plugin
rsconstruct processors list   # shows the plugin
rsconstruct processors files  # shows files discovered by the plugin

Lua API Contract

Each .lua file defines global functions. Three are required; the rest have sensible defaults.

Required Functions

description()

Returns a human-readable string describing what the processor does. Called once when the plugin is loaded.

function description()
    return "Lint JavaScript files with ESLint"
end

discover(project_root, config, files)

Called during product discovery. Receives:

  • project_root (string) — absolute path to the project root
  • config (table) — the [processor.NAME] TOML section as a Lua table
  • files (table) — list of absolute file paths matching the scan configuration

Must return a table of products. Each product is a table with inputs and outputs keys, both containing tables of absolute file paths.

function discover(project_root, config, files)
    local products = {}
    for _, file in ipairs(files) do
        local stub = rsconstruct.stub_path(project_root, file, "myplugin")
        table.insert(products, {
            inputs = {file},
            outputs = {stub},
        })
    end
    return products
end

execute(product)

Called to build a single product. Receives a table with inputs and outputs keys (both tables of absolute path strings). Must create the output files on success or error on failure.

function execute(product)
    rsconstruct.run_command("mytool", {product.inputs[1]})
    rsconstruct.write_stub(product.outputs[1], "done")
end

Optional Functions

clean(product)

Called when running rsconstruct clean. Receives the same product table as execute(). Default behavior: removes all output files.

function clean(product)
    for _, output in ipairs(product.outputs) do
        rsconstruct.remove_file(output)
    end
end

auto_detect(files)

Called to determine whether this processor is relevant for the project (when auto_detect = true in config). Receives the list of matching files. Default: returns true if the files list is non-empty.

function auto_detect(files)
    return #files > 0
end

required_tools()

Returns a table of external tool names required by this processor. Used by rsconstruct tools list and rsconstruct tools check. Default: empty table.

function required_tools()
    return {"eslint", "node"}
end

hidden()

Returns true to hide this processor from default rsconstruct processors list output (still shown with --all). Default: false.

function hidden()
    return false
end

processor_type()

Returns the type of processor: "generator" or "checker". Generators create real output files (e.g., compilers, transpilers). Checkers validate input files; for checkers, you can choose whether to produce stub files or not. Default: "checker".

Option 1: Checker with stub files (for Lua plugins)

function processor_type()
    return "checker"
end

When using stub files, return outputs = {stub} from discover() and call rsconstruct.write_stub() in execute().

Option 2: Checker without stub files

function processor_type()
    return "checker"
end

Return outputs = {} from discover() and don’t write stubs in execute(). The cache database entry itself serves as the success record.

The rsconstruct Global Table

Lua plugins have access to an rsconstruct global table with helper functions.

FunctionDescription
rsconstruct.stub_path(project_root, source, suffix)Compute the stub output path for a source file. Maps project_root/a/b/file.ext to out/suffix/a_b_file.ext.suffix.
rsconstruct.run_command(program, args)Run an external command. Errors if the command fails (non-zero exit).
rsconstruct.run_command_cwd(program, args, cwd)Run an external command with a working directory.
rsconstruct.write_stub(path, content)Write a stub file (creates parent directories as needed).
rsconstruct.remove_file(path)Remove a file if it exists. No error if the file is missing.
rsconstruct.file_exists(path)Returns true if the file exists.
rsconstruct.read_file(path)Read a file and return its contents as a string.
rsconstruct.path_join(parts)Join path components. Takes a table: rsconstruct.path_join({"a", "b", "c"}) returns "a/b/c".
rsconstruct.log(message)Print a message prefixed with the plugin name.

Configuration

Plugins use the standard scan configuration fields. Any [processor.NAME] section in rsconstruct.toml is passed to the plugin’s discover() function as the config table.

Scan Configuration

These fields control which files are passed to discover():

KeyTypeDefaultDescription
scan_dirstring""Directory to scan ("" = project root)
extensionsstring[][]File extensions to match
exclude_dirsstring[][]Directory path segments to skip
exclude_filesstring[][]File names to skip
exclude_pathsstring[][]Paths relative to project root to skip

Custom Configuration

Any additional keys in the [processor.NAME] section are passed through to the Lua config table:

[processor.eslint]
scan_dir = "src"
extensions = [".js", ".ts"]
max_warnings = 0          # custom key, accessible as config.max_warnings in Lua
fix = false               # custom key, accessible as config.fix in Lua
function execute(product)
    local args = {product.inputs[1]}
    if config.max_warnings then
        table.insert(args, "--max-warnings")
        table.insert(args, tostring(config.max_warnings))
    end
    rsconstruct.run_command("eslint", args)
    rsconstruct.write_stub(product.outputs[1], "linted")
end

Plugins Directory

The directory where RSConstruct looks for .lua files is configurable:

[plugins]
dir = "plugins"  # default

Plugin Name Resolution

The plugin name is derived from the .lua filename (without extension). This name is used for:

  • The [processor.NAME] config section
  • The [processor.NAME] config section in rsconstruct.toml
  • The out/NAME/ stub directory
  • Display in rsconstruct processors list and build output

A plugin name must not conflict with a built-in processor name (tera, ruff, pylint, cc_single_file, cppcheck, shellcheck, spellcheck, make). RSConstruct will error if a conflict is detected.

Incremental Builds

Lua plugins participate in RSConstruct’s incremental build system automatically:

  • Products are identified by their inputs, outputs, and a config hash
  • If none of the declared inputs have changed since the last build, the product is skipped
  • If the [processor.NAME] config section changes, all products are rebuilt
  • Outputs are cached and can be restored from cache

For correct incrementality, make sure discover() declares all files that affect the output. If your tool reads additional configuration files, include them in the inputs list.

Examples

A checker that validates files without producing stub files. Success is recorded in the cache database.

function description()
    return "Lint YAML files with yamllint"
end

function processor_type()
    return "checker"
end

function required_tools()
    return {"yamllint"}
end

function discover(project_root, config, files)
    local products = {}
    for _, file in ipairs(files) do
        table.insert(products, {
            inputs = {file},
            outputs = {},  -- No output files
        })
    end
    return products
end

function execute(product)
    rsconstruct.run_command("yamllint", {"-s", product.inputs[1]})
    -- No stub to write; cache entry = success
end

function clean(product)
    -- Nothing to clean
end
[processor.yamllint]
extensions = [".yml", ".yaml"]

Stub-Based Linter (Legacy)

A linter that creates stub files. Use this if you need the stub file for some reason.

function description()
    return "Lint YAML files with yamllint"
end

function processor_type()
    return "checker"
end

function required_tools()
    return {"yamllint"}
end

function discover(project_root, config, files)
    local products = {}
    for _, file in ipairs(files) do
        table.insert(products, {
            inputs = {file},
            outputs = {rsconstruct.stub_path(project_root, file, "yamllint")},
        })
    end
    return products
end

function execute(product)
    rsconstruct.run_command("yamllint", {"-s", product.inputs[1]})
    rsconstruct.write_stub(product.outputs[1], "linted")
end
[processor.yamllint]
extensions = [".yml", ".yaml"]

File Transformer (Generator)

A plugin that transforms input files into output files (not stubs). This is a “generator” processor.

function description()
    return "Compile Sass to CSS"
end

function processor_type()
    return "generator"
end

function required_tools()
    return {"sass"}
end

function discover(project_root, config, files)
    local products = {}
    for _, file in ipairs(files) do
        local out = file:gsub("%.scss$", ".css"):gsub("^" .. project_root .. "/src/", project_root .. "/out/sass/")
        table.insert(products, {
            inputs = {file},
            outputs = {out},
        })
    end
    return products
end

function execute(product)
    rsconstruct.run_command("sass", {product.inputs[1], product.outputs[1]})
end
[processor.sass]
scan_dir = "src"
extensions = [".scss"]