Primate LogoPrimate

Primate 0.44: guided upgrades, earlier route checks and JSON dictionaries

Today we're announcing Primate 0.44. This release makes upgrades easier, catches route mistakes during builds, and adds more flexible schemas, Store sequences and client navigation state.

If you're new to Primate, we recommend reading the quickstart page to get started.

See what to do before upgrading

Primate 0.44 adds a read-only command that tells you whether your application is up to date and, when a newer release is available, what to do next.

primate update

On 0.44, before the next release ships, the command says that you're up to date. Once the next release ships, it shows the steps for upgrading to that release. Complete those steps, upgrade your dependencies, and run the command again to check for the next upgrade.

The command does not change your application or install anything. Because it is new in 0.44, use this post's upgrade notes when moving from 0.43 to 0.44. See Update guidance for the full command contract.

Catch invalid body routes during production builds

Production builds now catch routes that declare a body schema without its content type, so you can fix the mistake before it reaches a running application.

import p from "pema";
import route from "primate/route";

const CreatePost = p({
  title: p.string,
  body: p.string,
});

export default route({
  post: route.with(
    { body: CreatePost, contentType: "application/json" },
    async request => {
      const post = await request.body.json();
      return { id: 1, ...post };
    },
  ),
});

Route modules still run their top-level initialization during this check, so their imports and setup must work on the build host. See Build validation and request body validation.

Require a top-level object for JSON fields

Use p.json().dict() when a JSON field must contain an object while its nested values may use any valid JSON shape.

import p from "pema";

const Preferences = p.json().dict();

Preferences.parse({
  theme: "dark",
  permissions: ["read", "write"],
  dashboard: { compact: true },
});

It rejects a top-level array or primitive but accepts arrays, objects and primitives inside the object.

Use p.dict() instead when every property must follow the same schema:

p.dict();          // Record<string, string>
p.dict(p.boolean); // Record<string, boolean>

See JSON values for the complete comparison.

Allocate portable Store sequences

Unsigned Pema integer fields can now generate portable, persistent sequence numbers:

const Event = store({
  table: "event",
  db,
  schema: {
    id: store.key.primary(p.uuid),
    number: p.u32.sequence(),
  },
});

await Event.insert({});              // number: 1
await Event.insert({ number: 10 });  // number: 10
await Event.insert({});              // number: 11

Omit the field to use the next number, or provide a higher value to advance the sequence. Counters work across batches and concurrent writers and survive row deletion. Gaps can occur after failed inserts, so do not use sequences where gapless numbering is required. The API is available on u8, u16, u32, u64 and u128. See integer sequences.

Carry private context with a history entry

Client navigation can now carry private, temporary context with a browser history entry without adding it to the URL.

import navigate from "primate/navigate";

await navigate(`/tasks/${task.id}`, {
  state: { returnTo: "/tasks?status=open" },
});

The destination reads the current value through navigate.state(). Completion listeners also receive it as state alongside source and url.

const unsubscribe = navigate.after(({ source, state, url }) => {
  console.log({ source, state, url });
});

State follows the entry through Back and Forward navigation but is absent from copied URLs, new tabs and full refreshes. Use it for temporary context such as a return destination, not persistent preferences. See client navigation state.

Resolve application aliases in config and migration builds

Aliases from compilerOptions.paths now work when Primate loads configuration, Stores and migration inputs.

{
  "compilerOptions": {
    "paths": {
      "@db": ["./config/db.ts"],
      "@stores/*": ["./stores/*"]
    }
  }
}
import db from "@db";

export default db;

This applies to code loaded by Primate. Scripts run directly by Node still need relative imports or package imports beginning with #. See Database configuration.

Run Go applications from installed packages

Installed @primate/go packages now load their bundled WebAssembly support correctly, so importing @primate/go/env no longer fails before the application starts. This fix is also available to Primate 0.43 applications in @primate/go@0.12.1. See the Go backend guide.

Upgrade notes

Migrate from the retired HTML and Web Components frontends

Primate no longer includes the @primate/html and @primate/webc frontends. Applications using .html or .webc views should migrate those views to any other supported frontend, such as Marko, HTMX, Voby, React or Svelte. Remove the retired frontend modules from the application configuration and package manifest after migrating every view.

Add contentType beside body schemas

Search route declarations for route.with options that contain body but omit contentType. Production builds now reject those declarations. Add the media type that matches how the handler reads the body, commonly application/json for request.body.json().

route.with(
  { body: Input, contentType: "application/json" },
  handler,
);

Choose the intended dictionary contract

Existing p.json() schemas continue to accept every JSON-compatible top-level shape. Add .dict() only when arrays and primitives should be rejected at the outer boundary. Keep using p.dict(value) when every property value must pass the same non-JSON-field schema.

Keep direct Node imports resolvable

Aliases configured in tsconfig.json work in the Primate config and migration bundle, but not in a module launched directly by Node. Keep direct scripts on relative or # package imports instead of relying on an application alias.

Fin

If you like Primate, consider joining our Discord server, visiting us in #primate on irc.libera.chat, or starring us on GitHub.