Build a minimal HashiCorp plugin binary, install it into a plugin directory, and call it from the REPL.
This tutorial walks through the full happy path for building and installing a plugin for go-go-goja.
By the end, you will have:
goja-repl tui can scan,require("plugin:...") call from JavaScript,This tutorial uses the user-facing example plugin under plugins/examples/greeter as the fastest route to success first, and then shows the richer SDK-based code shape you should use for your own plugin.
The repo now also ships additional examples under plugins/examples/...:
clock for structured return values,validator for sdk.Call helper usage and validation errors,kv for stateful object methods,system-info for nested mixed-shape responses,failing for explicit handler errors.Work through greeter first, then read the others as focused follow-up examples.
You need:
go run ./cmd/goja-repl tui,/tmp/goja-plugins.This tutorial assumes you are running commands from the repository root.
If you want to constrain the runtime to one expected plugin module while testing, you can add --allow-plugin-module plugin:examples:greeter to the REPL commands shown below.
Start by building the existing example. This proves the host side is working before you start editing your own code.
mkdir -p ~/.go-go-goja/plugins/examples
go build -o ~/.go-go-goja/plugins/examples/goja-plugin-examples-greeter ./plugins/examples/greeter
If you want all example plugins instead of only the baseline one, you can also run:
make install-modules
Why this step matters:
The output file name should match the host discovery pattern. The default pattern is goja-plugin-*, so goja-plugin-examples-greeter is a safe default.
Run:
go run ./cmd/goja-repl tui
This tells the runtime builder to:
~/.go-go-goja/plugins/...,If startup succeeds, you now have a runtime that can resolve plugin modules through require().
Inside the REPL:
let greeter = require("plugin:examples:greeter")
If this line works, the main integration path is already correct:
Continue in the REPL:
greeter.greet("hello")
greeter.strings.upper("hello")
greeter.meta.pid()
Expected results:
"hello, hello""HELLO"At this point you have verified both supported export styles:
The current recommended path is to author plugins with pkg/hashiplugin/sdk.
That gives you four useful building blocks:
sdk.MustModule(...) to define one plugin module,sdk.Function(...) for top-level exports,sdk.Object(...sdk.Method(...)) for object-method exports,sdk.Serve(...) to boot the shared transport.For richer documentation, methods can also declare:
sdk.MethodSummary(...) for the short one-line description used by compact UIs,sdk.MethodDoc(...) for the fuller body,sdk.MethodTags(...) for simple search/display labels.The minimal useful shape now looks like this:
package main
import (
"context"
"fmt"
"strings"
"github.com/go-go-golems/go-go-goja/pkg/hashiplugin/sdk"
)
func main() {
mod := sdk.MustModule(
"plugin:hello",
sdk.Version("v1"),
sdk.Doc("Simple hello plugin"),
sdk.Function("greet", func(_ context.Context, call *sdk.Call) (any, error) {
name := call.StringDefault(0, "world")
return fmt.Sprintf("hello, %s", name), nil
}),
sdk.Object("strings",
sdk.Method(
"upper",
func(_ context.Context, call *sdk.Call) (any, error) {
return strings.ToUpper(call.StringDefault(0, "")), nil
},
sdk.MethodSummary("Uppercase the first argument"),
sdk.MethodDoc("Uppercase the first argument"),
sdk.MethodTags("strings", "uppercase"),
),
),
)
sdk.Serve(mod)
}
This is the new mental model:
sdk.MustModule(...) defines the plugin module and its manifest shape.sdk.Function(...) and sdk.Object(...sdk.Method(...)) declare exports.sdk.MethodSummary(...), sdk.MethodDoc(...), and sdk.MethodTags(...) enrich object methods for docs/search/help.sdk.Call gives handlers easy access to decoded arguments.sdk.Serve(...) publishes the service over the shared transport.Put your plugin in a package of your choice and build it into the plugin directory.
Example:
go build -o ~/.go-go-goja/plugins/examples/goja-plugin-my-module ./path/to/your/plugin
Make sure:
goja-plugin-*,plugin:,If your manifest name is plugin:hello, test it like this:
const hello = require("plugin:hello")
hello.greet("Manuel")
This should now behave exactly like any other runtime-registered module from the JavaScript side.
If you want namespaced methods, add an object export with methods:
sdk.Object("math",
sdk.Method("add", func(_ context.Context, call *sdk.Call) (any, error) {
a, err := call.Float64(0)
if err != nil {
return nil, err
}
b, err := call.Float64(1)
if err != nil {
return nil, err
}
return a + b, nil
}),
)
From JavaScript:
const mod = require("plugin:hello")
mod.math.add(2, 3)
This is the right approach when you want one module to expose several closely related operations without flattening every function into the top-level export object.
This first implementation is intentionally conservative.
Plan around these constraints:
goja-repl tui is the canonical interactive path for plugin testing,goja-repl tui defaults to scanning ~/.go-go-goja/plugins/....These are not permanent limits, but they are the limits you should design against today.
Here is the full shell sequence in one place:
mkdir -p ~/.go-go-goja/plugins/examples
go build -o ~/.go-go-goja/plugins/examples/goja-plugin-examples-greeter ./plugins/examples/greeter
go run ./cmd/goja-repl tui
Then inside the TUI:
const greeter = require("plugin:examples:greeter")
greeter.greet("hello")
greeter.strings.upper("hello")
greeter.meta.pid()
That sequence is a good smoke test after host-side changes or when onboarding a new plugin author.
After you finish the baseline flow, the next useful follow-up commands are:
go build -o ~/.go-go-goja/plugins/examples/goja-plugin-examples-validator ./plugins/examples/validator
go build -o ~/.go-go-goja/plugins/examples/goja-plugin-examples-kv ./plugins/examples/kv
Then in JavaScript:
const validator = require("plugin:examples:validator")
validator.grade(0.9, true)
const kv = require("plugin:examples:kv")
kv.store.set("name", "Manuel")
kv.store.get("name")
| Problem | Cause | Solution |
|---|---|---|
require("plugin:hello") fails | The binary was not discovered or the manifest did not match the requested module name | Check the output filename, the plugin directory path, and the ModuleName field in the manifest |
| Runtime startup fails before the REPL prompt appears | Plugin validation failed during runtime creation | Build with a known-good manifest first, then compare your plugin to the example fixture |
| A handler returns the wrong value shape | The SDK could not encode the returned Go value into structpb.Value | Stick to strings, numbers, booleans, arrays, objects, and null |
| The plugin works in tests but not manually | The test built the binary into a temp dir, but your manual run expects the default per-user tree | Rebuild into ~/.go-go-goja/plugins/... or pass the exact path with --plugin-dir |
goja-repl tui does not see plugins | The binary was not built under the default tree and no explicit directory was passed | Build into ~/.go-go-goja/plugins/... or start goja-repl tui with --plugin-dir /path/to/plugins |
goja-repl help goja-plugin-user-guide — User-facing plugin referencegoja-repl help goja-plugin-developer-guide — Internal architecture and integration guidegoja-repl help repl-usage — General REPL usage