Tutorial: build and install a go-go-goja plugin

Build a minimal HashiCorp plugin binary, install it into a plugin directory, and call it from the REPL.

Sections

Terminology & Glossary
📖 Documentation
Navigation
31 sectionsv0.1
📄 Tutorial: build and install a go-go-goja plugin — glaze help plugin-tutorial-build-install
plugin-tutorial-build-install

Tutorial: build and install a go-go-goja plugin

Build a minimal HashiCorp plugin binary, install it into a plugin directory, and call it from the REPL.

Tutorialgojapluginstutorialreplhashicorpjavascriptgoja-repl--plugin-dir

This tutorial walks through the full happy path for building and installing a plugin for go-go-goja.

By the end, you will have:

  • a plugin binary built from Go source,
  • a plugin directory that goja-repl tui can scan,
  • a working require("plugin:...") call from JavaScript,
  • a mental model for how your Go code becomes a JavaScript module.

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.

Prerequisites

You need:

  • a working Go toolchain,
  • this repository checked out,
  • the ability to run go run ./cmd/goja-repl tui,
  • a writable local directory such as /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.

Step 1: Build the example plugin

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:

  • it gives you a known-good binary,
  • it verifies your local Go environment,
  • it gives you a reference binary name and location for discovery.

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.

Step 2: Start the REPL with plugin discovery enabled

Run:

go run ./cmd/goja-repl tui

This tells the runtime builder to:

  • scan ~/.go-go-goja/plugins/...,
  • launch matching plugin binaries,
  • validate their manifests,
  • register valid plugin modules into the runtime.

If startup succeeds, you now have a runtime that can resolve plugin modules through require().

Step 3: Require the plugin module

Inside the REPL:

let greeter = require("plugin:examples:greeter")

If this line works, the main integration path is already correct:

  • the binary was discovered,
  • the plugin handshake succeeded,
  • the manifest was accepted,
  • the module was registered into the runtime.

Step 4: Call exported functions

Continue in the REPL:

greeter.greet("hello")
greeter.strings.upper("hello")
greeter.meta.pid()

Expected results:

  • "hello, hello"
  • "HELLO"
  • a numeric process ID

At this point you have verified both supported export styles:

  • direct function export,
  • object method export.

The current recommended path is to author plugins with pkg/hashiplugin/sdk.

That gives you four useful building blocks:

  1. sdk.MustModule(...) to define one plugin module,
  2. sdk.Function(...) for top-level exports,
  3. sdk.Object(...sdk.Method(...)) for object-method exports,
  4. 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.

Step 6: Build your own plugin

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:

  • the output filename still matches goja-plugin-*,
  • the manifest module name begins with plugin:,
  • the SDK declarations match the exports you expect JavaScript to see.

Step 7: Load your plugin in JavaScript

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.

Step 8: Add an object export

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.

Step 9: Know the current limits

This first implementation is intentionally conservative.

Plan around these constraints:

  • values should be JSON-like,
  • manifests only support function and object exports,
  • 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.

Complete reference workflow

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")

Troubleshooting

ProblemCauseSolution
require("plugin:hello") failsThe binary was not discovered or the manifest did not match the requested module nameCheck the output filename, the plugin directory path, and the ModuleName field in the manifest
Runtime startup fails before the REPL prompt appearsPlugin validation failed during runtime creationBuild with a known-good manifest first, then compare your plugin to the example fixture
A handler returns the wrong value shapeThe SDK could not encode the returned Go value into structpb.ValueStick to strings, numbers, booleans, arrays, objects, and null
The plugin works in tests but not manuallyThe test built the binary into a temp dir, but your manual run expects the default per-user treeRebuild into ~/.go-go-goja/plugins/... or pass the exact path with --plugin-dir
goja-repl tui does not see pluginsThe binary was not built under the default tree and no explicit directory was passedBuild into ~/.go-go-goja/plugins/... or start goja-repl tui with --plugin-dir /path/to/plugins

See Also

  • goja-repl help goja-plugin-user-guide — User-facing plugin reference
  • goja-repl help goja-plugin-developer-guide — Internal architecture and integration guide
  • goja-repl help repl-usage — General REPL usage