the effing blog

Effing 0.40.0: render, hold the server

We've added a render command that turns a fn into a file with zero setup, a canonical annie reader, and instance-scoped fn runtimes.

Effing 0.40.0 is out. The headline is a new CLI command, effing render, that turns a fn into a file with one command and zero setup.

Render straight to a file

Until now, getting a fn’s output onto disk meant having things running: start effing dev and save what the preview serves, or mint a signed URL with effing url — which requires a SECRET_KEY in your env — and fetch it. Fine once a project is set up, but a lot of ceremony when you just want to see the thing.

Now this does it all in one shot:

npx --no effing render effie my-video -o out.mp4

Under the hood it spins up an ephemeral dev server on a free loopback port, resolves the fn with its previewProps (or whatever you pass via --props), writes the artifact, and tears the server down again. Images come out as PNG or JPEG following the fn’s encoding, annies as a TAR of frames, and effies as an MP4 — that last one delegates to the project-local ffs render, so effies need @effing/ffs installed.

The zero-setup part extends to secrets. A missing SECRET_KEY is replaced by a throwaway key for the duration of the render, and effing dev now does the same, so neither command needs any env setup at all. The only cost in dev is that copied URLs don’t survive a restart, which the startup log calls out. effing url and the built production server still require a configured key, since their URLs must stay valid beyond a single process.

If you point coding agents at your effing project, this is the command to teach them: it’s the zero-setup way to check what a fn actually produces, and effing manual now says exactly that.

One annie reader, owned by the format

An annie is a TAR archive of image frames, and @effing/annie has always known how to write one. Reading one back was another story: the package that owns the format had no reader, so @effing/ffs hand-rolled one on tar-stream and @effing/annie-player hand-rolled another on untar.js, each re-deriving the frame naming and ordering rules on its own.

0.40.0 adds annieFrames(), a canonical reader that async-iterates the frames of an annie:

import { annieFrames } from "@effing/annie";

for await (const frame of annieFrames(source)) {
  console.log(frame.index, frame.contentType, frame.data.length);
}

The source can be bytes, an (async) iterable of byte chunks — a Node.js Readable works as-is — or a Web ReadableStream. Frames arrive in ascending index order, each carrying its raw bytes and a content type sniffed from the frame’s magic bytes, since the archive itself doesn’t record the image format. Malformed archives — truncated, corrupt checksums, no frames at all — are rejected with clear errors instead of producing half an animation.

Both former hand-rolled readers are gone. For the player this is more than a cleanup: the archive now streams from the response body instead of being buffered whole, frames are ordered by their actual index rather than a name sort (which mis-ordered unpadded names), and each frame’s Blob carries its sniffed content type. In ffs, entry names are now validated against the canonical frame_<digits> pattern before being used as filenames, closing a path-traversal window for crafted archives.

Instance-scoped fn runtimes

@effing/fn kept its module loader and URL builder in module-global state set by initFnRuntime(), so a process could hold exactly one fn runtime configuration — hidden state that parallel tests and multi-tenant servers had to serialize around.

The new createFnRuntime() returns a handle that captures its loader and builder in a closure, so independent runtimes can coexist in one process:

import { createFnRuntime } from "@effing/fn";

const runtime = createFnRuntime({ moduleLoader, urlBuilder });
const module = await runtime.fnModule("image", "my-image");

The global API — initFnRuntime(), fnModule(), fnUrl() and friends — is unchanged and now delegates to a global runtime under the hood, so existing code keeps working as-is.

Upgrading

Bump your @effing/* packages to 0.40.0. Everything above is additive or backward compatible, with one edge: the annie player now accepts only canonical annies, so archives with entries not named frame_<digits> are rejected. If your annies come from @effing/annie — and they almost certainly do — nothing changes. The full per-package changelogs are in the release PR.