Release 0.26: Go Wasm routes, live reload, and HTMX extensions

Today we're announcing the availability of the Primate 0.26 preview release. This release introduces support for Go Wasm routes, live reload using the new @primate/build module, and HTMX extensions.

If you're new to Primate, we recommend reading the Getting started page to get an idea of the framework.

Go Wasm routes

Primate is one of the first frameworks to offer writing backend logic in different languages. In addition to its native support of JavaScript, you can now write your route functions in Go, using a nearly identical API. This gives you access to Go's standard library, but also the entire Go ecosystem when writing backend logic.

We believe this is an important step towards allowing more developers access to frontend frameworks with all their features -- SSR, hydration, and client side rendering.

Install

To add support for Go, install the @primate/binding module.

npm install @primate/binding

In addition, your system needs to have the go executable in its path, as it is used to compile Go routes into WebAssembly.

Configure

Import and initialize the module in your configuration.

primate.config.js
import { go } from "@primate/binding";

export default {
  modules: [
    go(),
  ],
};

Use

When writing routes, you can pretty much do everything you can do in JavaScript routes, in Go. For example, if you return strings or maps from your Go route, Primate will serve them as content type text/plain and application/json, respectively.

routes/index.go
func Get(request Request) any {
  return "Donald";
}

func Post(request Request) any {
  return map[string]any{
    "name": "Donald",
  };
}

If you send a GET request to /, you will see a plain text response of "Donald". For POST, you'll see a JSON response with {"name": "Donald"}.

For your convenience, Primate furnishes you with two types, Object which is map[string]any and Array which is []any. If you're returning a JSON containing an array, you would write the following route.

routes/json-array.go
func Get(request Request) any {
  return Array{
    Object{ "name": "Donald" },
    Object{ "name": "Ryan" },
  };
}

Accessing GET at /json-array will return a JSON array with this data.

[
  { "name": "Donald" },
  { "name": "Ryan" }
]

In addition, much like with JavaScript routes, you have access to a Request object that exposes several properties.

Request struct
type Request struct {
  Url URL
  Body Dispatcher
  Path Dispatcher
  Query Dispatcher
  Cookies Dispatcher
  Headers Dispatcher
  // dynamic properties
}

A Dispatcher, much like in JavaScript, allows you to query for subproperties of the field.

Dispatcher struct
type Dispatcher struct {
  Get func(string) any
  All func() map[string]any
  // dynamic runtime type getters
}

For example, if a GET request is sent to /?name=Donald, it could be served by the following route, returning the value of the query string parameter name as plain text.

routes/index.go
func Get(request Request) any {
  // on GET /?name=Donald -> responds with text/plain "Donald"
  return request.Query.Get("name").(string);
}

For the full documentation of Go routes, see the Go module documentation.

Future of WebAssembly in Primate

With Go Wasm supported in Primate, we are working on supporting additional programming languages in Primate and improving the Go API compatibility to match that of JavaScript. As WASI becomes more mature and is supported by more programming languages and runtimes, we intend to move to that.

If you have a particular language you wish to see supported in Primate routes, please open an issue describing your use case.

Live reload

This release features a rewritten @primate/build module, formerly named @primate/esbuild. The esbuild export of this module now bundles your application both in development (npx primate) and production (npx primate serve) mode. In development mode, this module will insert a small client-side script that reloads the browser whenever you edit and save a component.

For more information, see the @primate/build documentation page.

HTMX extensions

Primate now supports HTMX extensions, using an ESM version of HTMX (htmx-esm package) instead of the htmx.org package.

To use an HTMX extension, pass it to the HTMX module's extensions array property in your Primate configuration.

import { htmx } from "@primate/frontend";

export default {
  modules: [
    htmx({
      extensions: ["head-support"],
    }),
  ],
};

If you're using the client-side-templates extension, include the individual client side templates in the client_side_templates array property.

import { htmx } from "@primate/frontend";

export default {
  modules: [
    htmx({
      extensions: ["client-side-templates", "head-support"],
      client_side_templates: ["handlebars", "mustache"],
    }),
  ],
};

Migrating from 0.25

Use all() instead of get()

Change any uses of request.{body, path, query, cookies, headers}.get without parameters to all. Previously, the get function on a dispatcher could be called with a property, in which case it would retrieve that property's value (or undefined), or without a property, which would return the entire underlying object.

The latter case has been now extracted from get into all. Calling dispatcher.get without a string argument in 0.26 will throw.

New: request.session.all()

A request.session object is now similar to a dispatcher in that that it has both get and all, to query individual session properties and the entire session data. Like real dispatchers, get will throw if called without arguments or with a non-string first argument.

Depend on htmx-esm instead of htmx.org

As HTMX extensions are now supported, HTMX had to be repackaged in an ESM friendly way that allows bundling extensions. If you use the @primate/frontend module with HTMX, make sure to remove the htmx.org dependency and install htmx-esm instead. Primate will error out if you're missing htmx-esm.

Use @primate/build instead of @primate/esbuild

The @primate/esbuild package has been renamed @primate/build. If you previously used @primate/esbuild with its default export, install @primate/build and change your configuration to use the esbuild export of @primate/build. Remove the old @primate/esbuild package.

// old: import esbuild from "@primate/esbuild";
import { esbuild } from "@primate/build";

export default {
  modules: [
    esbuild(),
  ],
};

As esbuild has become a peer dependency, you will also now need to install it explicitly using npm install esbuild. Primate will error out if you're missing it.

Convert type functions to type objects

In 0.26, it is no longer possible to export a runtime type as a function. All runtime types must instead export an object with base string and a validate function property. Change any runtime type functions to the new object format.

const uuid = /^[^\W_]{8}-[^\W_]{4}-[^\W_]{4}-[^\W_]{4}-[^\W_]{12}$/u;

// before
/*
export default value => {
  if (uuid.test(value)) {
    return value;
  }

  throw new Error(`${JSON.stringify(value)} is not a valid UUID`);
};
*/

// now
export default {
  base: "string",
  validate(value) {
    if (uuid.test(value)) {
      return value;
    }

    throw new Error(`${JSON.stringify(value)} is not a valid UUID`);
  },
};

Other changes

Consult the full changelog for a list of all relevant changes.

Next on the road

Some of the things we plan to tackle in the upcoming weeks are,

This list isn't exhaustive or binding. None, some or all of these features may be included in 0.27, and other features may be prioritized according to feedback.

Fin

If you like Primate, consider joining our channel #primate on irc.libera.chat.

Otherwise, have a blast with the new version!