Documentation
¶
Overview ¶
esbuild.go wraps esbuild's pure-Go API (no CGO) so zip can compile TS / modern-JS handler source down to ES5 that the embedded goja VM executes. This is the build step of zip's TS migration path:
TS source --esbuild target=es5--> ES5 JS --drop into--> embedded goja
Run TranspileToES5 at service startup (or at build time) to compile bundled handlers before serving; the resulting bytes are handed to JSRuntime.LoadModule / Eval.
handler.go bridges a goja-loaded JS function into Fiber. It returns fiber.Handler (not zip.Handler) on purpose: the zip root package imports this package, so depending back on zip would be an import cycle. fiber.Handler mounts cleanly anywhere — app.Fiber().Get(...) or any *fiber.App — and is the natural seam for legacy Express-shaped handlers running inside the embedded VM.
The req/res objects handed to JS mirror the Express shape so existing `function (req, res) { res.json(...) }` handlers run unmodified:
req.method req.path req.query req.headers req.body res.status(n) res.set(k,v) res.json(v) res.send(v)
jsvm.go embeds a pure-Go JavaScript runtime (goja) into zip so a service can run TS/JS handlers in-process — no separate runtime service, no inter-service RPC, no container-per-service. Combined with esbuild.go (TS/modern-JS → ES5) this is zip's migration path: legacy TS source compiles to ES5 and drops straight into the embedded VM, then gets rewritten to a native Go handler in place over time.
The pool pattern is lifted from hanzoai/base/plugins/gojavm and slimmed to what zip needs: each *goja.Runtime carries the host functions and modules registered at construction time, and requests borrow a free VM (or a one-off when the pool is saturated) so they never pay per-request VM creation cost.
runner.go is zip's unified multi-language code runner. A Runner holds a registry of language → Engine and executes a source string against the engine registered for a language, each call in its own goroutine, bound to a context.Context. It generalizes the goroutine-scoped determinism contract of JSRuntime.EvalContext (one worker, one watcher, joined before return, engine-specific interrupt on cancel) across every language backend the host registers.
DEPENDENCY DIRECTION. zip does NOT import hanzoai/base. The Engine interface here is a zip-side projection — duck-typed, exactly like the Loader/Module projection in package internal/runtime. base's backends (gojavm, pyvm, v8vm, wasmvm, starkvm) implement an extruntime.Runtime SPI; a thin adapter at the host's app-startup makes each satisfy this Engine and registers it. base imports zip and registers; zip exposes the registry and never names base. That is the whole point of the SPI: the registry is the seam, not an import edge.
The one in-tree Engine zip ships is the goja engine — zip's own *JSRuntime, surfaced via (*JSRuntime).Engine(). It is pure Go, no cgo, and is the canonical "js" backend. Everything else plugs in from base.
Package runtime is the zip-side projection of HIP-0105's extension runtime contract. Consumers of zip pass a Loader implementation here (typically *extruntime.Loader from hanzoai/base/plugins/extruntime) and zip mounts modules as routes via app.Module().
Index ¶
- Variables
- func BundleToES2015(entry string, files map[string][]byte, opts BundleOptions) ([]byte, error)
- func JSHandler(rt *JSRuntime, fnName string) fiber.Handler
- func JSModule(rt *JSRuntime, modulePath string) (fiber.Handler, error)
- func TranspileToES5(src []byte, opts ESOptions) ([]byte, error)
- type BundleOptions
- type ESOptions
- type Engine
- type Interruptible
- type JSOptions
- type JSRuntime
- func (rt *JSRuntime) Engine() Engine
- func (rt *JSRuntime) Eval(src string) (any, error)
- func (rt *JSRuntime) EvalContext(ctx context.Context, src string) (any, error)
- func (rt *JSRuntime) InvokeFunc(ctx context.Context, fnName string, args ...any) (any, error)
- func (rt *JSRuntime) LoadModule(name, src string) error
- func (rt *JSRuntime) RegisterHostFn(name string, fn any) error
- type Loader
- type Module
- type Runner
Constants ¶
This section is empty.
Variables ¶
var ErrUnknownLanguage = errors.New("zip/runtime: unknown language")
ErrUnknownLanguage is returned by Run when no engine is registered for the requested language.
Functions ¶
func BundleToES2015 ¶
BundleToES2015 transpiles and bundles a multi-file TS/JS tree rooted at entry into a single ES2015 string ready for goja. files maps virtual paths (e.g. "entry.js", "lib/util.ts") to their source bytes; entry is the key of the root module. Relative imports between files are resolved against the importer's directory within the files map — no real filesystem access occurs. Packages listed in opts.External are left as require()/import calls. Returns a non-nil error if esbuild reports any error-level diagnostic (including an unresolved import).
func JSHandler ¶
JSHandler returns a fiber.Handler that invokes the global JS function named fnName (defined in the runtime via Eval / LoadModule) with an Express-shaped (req, res) pair. State written through res is propagated back onto the Fiber Ctx after the call.
func JSModule ¶
JSModule loads a CommonJS module whose `module.exports` is the handler function (Express shape) and returns a fiber.Handler for it. modulePath is the name the module was registered under via LoadModule.
func TranspileToES5 ¶
TranspileToES5 compiles src (TS or modern JS) to ES5 JavaScript ready for goja. The output is CommonJS-format so a module that does `module.exports = handler` can be loaded via JSRuntime.LoadModule and resolved with require(). Returns a non-nil error if esbuild reports any error-level diagnostic.
Types ¶
type BundleOptions ¶
type BundleOptions struct {
// Format is the output module format: "iife", "cjs", or "esm". Empty
// defaults to "cjs", the format goja loads via require().
Format string
// Sourcemap inlines a base64 source map into the output for debugging.
Sourcemap bool
// Minify enables identifier/whitespace/syntax minification.
Minify bool
// External lists import paths (typically bare package specifiers like
// "external" or "node:fs") that are left as require()/import calls in
// the output instead of being bundled. Resolution of these is the
// host's responsibility (e.g. a goja module registered under the same
// name).
External []string
}
BundleOptions configures a multi-file bundle. Zero value bundles to CommonJS (the format goja's require() understands) with no source map and no minification.
type ESOptions ¶
type ESOptions struct {
// Loader selects how the source is parsed. "ts", "tsx", "jsx", or
// "js". Empty defaults to "ts" (TS is a superset of JS, so plain JS
// also parses).
Loader string
// Minify enables identifier/whitespace minification.
Minify bool
// Sourcefile is the logical filename used in error messages.
Sourcefile string
}
ESOptions configures a transpile. Zero value is a sane default: TypeScript loader, ES5 target, no minification, CommonJS format so `module.exports = ...` survives into the goja-loadable output.
type Engine ¶
type Engine interface {
// Eval evaluates src and returns the engine-native result value
// (already converted to a Go value, e.g. goja's Export()). args are
// passed through to engines that accept invocation arguments;
// engines that only evaluate a top-level expression ignore them.
//
// Eval MUST respect ctx: at minimum it returns promptly once ctx is
// done. Interruptible engines additionally honor Interrupt.
Eval(ctx context.Context, src []byte, args ...any) (any, error)
}
Engine executes source in one language. It is the zip-side projection of base's extruntime backends; any value implementing Eval satisfies it, so base registers its backends without zip importing base.
An Engine MAY additionally implement Interruptible. When it does, the Runner's watcher goroutine calls Interrupt(ctx.Err()) on cancellation so a runaway evaluation unwinds instead of leaking. When it does not, the watcher cannot abort a running call — the Run still returns ctx.Err() promptly, but the underlying worker runs to completion. The Runner logs a one-time warning per such engine at registration.
type Interruptible ¶
type Interruptible interface {
Interrupt(cause error)
}
Interruptible is implemented by engines whose running evaluation can be aborted out-of-band (goja vm.Interrupt, pyvm PyErr_SetInterrupt, wasm trap). The Runner's watcher calls Interrupt with ctx.Err() on cancel.
type JSOptions ¶
type JSOptions struct {
// PoolSize is the number of pre-warmed *goja.Runtime kept hot. When
// every pooled VM is busy, calls fall back to a freshly-built VM that
// is discarded after the call. 0 selects a default of 8.
PoolSize int
// HostFns are Go functions exposed to JS as globals. Applied to every
// VM in the pool (and to one-off VMs). Equivalent to calling
// RegisterHostFn for each entry after construction, but applied to
// the whole pool atomically at build time.
HostFns map[string]any
// Modules are CommonJS-style module sources registered into every VM,
// reachable from JS via require(name). Applied at build time.
Modules map[string]string
}
JSOptions configures a JSRuntime.
type JSRuntime ¶
type JSRuntime struct {
// contains filtered or unexported fields
}
JSRuntime is an embedded JavaScript runtime backed by a pool of goja VMs. It is safe for concurrent use: each call borrows an isolated VM.
func NewJSRuntime ¶
NewJSRuntime builds a JSRuntime with the given options. Host functions and modules from opts are applied to every VM in the pool.
func (*JSRuntime) Engine ¶
Engine adapts this JSRuntime to the Runner's Engine interface so it can be registered as a language backend:
runner.Register("js", rt.Engine())
The adapter is deliberately NOT Interruptible at the Runner seam: EvalContext already owns the per-call interrupt watcher and targets the exact pooled VM the call borrowed (interrupting only that VM, never a sibling concurrent call's VM). A Runner-level Interrupt would have to fan out to every pooled VM and could abort unrelated concurrent calls. So the goja engine self-manages cancellation inside Eval; the Runner's watcher is a no-op for it (and the registration warning is suppressed because Eval honors ctx fully). Engines without internal ctx handling implement Interruptible and let the Runner's watcher abort them.
func (*JSRuntime) Eval ¶
Eval evaluates src in a pooled VM and returns the exported Go value of the result (via goja's Export()). It is EvalContext with a background context — the evaluation cannot be cancelled.
func (*JSRuntime) EvalContext ¶
EvalContext evaluates src in a pooled VM under ctx and returns the exported Go value of the result. If ctx is cancelled or its deadline is exceeded before the evaluation completes, the running goja VM is interrupted and EvalContext returns ctx.Err() (context.Canceled or context.DeadlineExceeded). goja is single-threaded per borrowed VM, so a tight loop like `while(true){}` is preempted at the next interrupt check point rather than blocking the caller indefinitely.
func (*JSRuntime) InvokeFunc ¶
InvokeFunc calls the global JS function named fnName with args (each converted to a goja value) and returns the exported Go value of its result. Like EvalContext, the call is interrupted and ctx.Err() is returned if ctx finishes before the function does. The named value must already be defined in the runtime (via Eval/EvalContext or LoadModule).
func (*JSRuntime) LoadModule ¶
LoadModule registers a CommonJS-style module under name. The module source is evaluated lazily the first time require(name) is called in a given VM. Applied to every pooled VM.
func (*JSRuntime) RegisterHostFn ¶
RegisterHostFn exposes a Go function to JS as a global named name. The function is applied to every VM in the pool and to one-off VMs built afterwards. fn may be any value goja can bind (typically a Go func).
type Loader ¶
Loader is re-exported from internal/runtime for ergonomic use:
loader := myextruntime.NewLoader(...) // implements zipruntime.Loader
app := zip.New(zip.Config{Loader: loader})
type Runner ¶
type Runner interface {
// Run looks up the engine for lang, spawns a worker goroutine that
// calls engine.Eval, and a watcher goroutine that interrupts the
// engine when ctx is done. It returns the engine result, or ctx.Err()
// if cancellation won the race. Both goroutines are joined before Run
// returns. If no engine is registered for lang it returns
// ErrUnknownLanguage.
Run(ctx context.Context, lang string, src []byte, args ...any) (any, error)
// Register installs engine under lang. Re-registering a language
// replaces the prior engine. Registration is concurrency-safe.
Register(lang string, engine Engine) error
// Languages returns the registered language names, sorted.
Languages() []string
}
Runner executes arbitrary-language code in goroutine-scoped, ctx-bound tasks. Safe for concurrent use: Run, Register and Languages may all be called from many goroutines.