> ## Documentation Index
> Fetch the complete documentation index at: https://narrator.ami.rip/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin versioning

> Declare which library versions a plugin reads, and handle breaking majors.

Libraries change. A method can be renamed, removed, or keep its name and change its meaning. A plugin that narrates the old meaning for new code is worse than no plugin, so Narrator gives plugins three tools: a declared version range, the major version each package declares, and an option for pinning a major version by hand.

## Declare the library you read

Set `library` to the npm package your plugin reads and the semver range you wrote it against.

```ts theme={null}
export const hono = (): TsPlugin<HonoPhrases> =>
  definePlugin({
    name: "hono",
    library: { name: "hono", versions: ">=4 <6" },
    phrases: { en },
    calls,
    statements,
  });
```

Every plugin in the repository declares its range. The field is metadata: the engine doesn't read it, but tools can, for example to warn when a project's installed version is outside the range. Declaring it costs nothing.

## Handle breaking majors

When two majors of a library use the same name for different things, the plugin can't tell from the code alone which one it is reading. It can tell from the file's package, though. When the narrator has a [dependency index](/concepts/dependency-detection), `e.libraryMajor("effect")` returns the major version the nearest `package.json` declares, for example `3` for `"^3.10.0"`.

Plugins whose libraries changed meaning also take a `version` option, which wins over the package:

```ts theme={null}
effect({ version: 4 });
elysia({ version: 2 });
```

When neither is known, the plugin runs in **auto** mode. Auto mode reads the names from both majors, and for a name whose meaning changed it uses a phrasing written to be true in either version. So the order is: the pinned `version`, then the package's declared major, then auto.

| Plugin | Majors | What differs |
| - | - | - |
| [Effect](/plugins/effect) | 3, 4 | `Schema.Date`, `Effect.catch` and other renamed or repurposed APIs |
| [Elysia](/plugins/elysia) | 1, 2 | Some route signatures and hook names |
| [AI SDK](/plugins/ai-sdk) | 6, 7 | Which steps `usage` and `toolCalls` cover on a multi-step result |
| [TanStack Query](/plugins/tanstack-query) | 4, 5 | `isLoading` and the query callbacks removed in v5 |
| [Fastify](/plugins/fastify) | 4, 5 | The argument order of `reply.redirect` |

Here is the same Effect schema read with each setting. In Effect 3, `Schema.Date` decodes a date from a string. In Effect 4 it is a date value.

```ts Effect 3 theme={null}
import { Schema } from "effect";
const Meeting = Schema.Struct({ title: Schema.String, at: Schema.Date });
```

```text English theme={null}
Let meeting be a schema for an object with title (text) and at (a date written as text).
```

```ts Effect 4 theme={null}
import { Schema } from "effect";
const Meeting = Schema.Struct({ title: Schema.String, at: Schema.Date });
```

```text English theme={null}
Let meeting be a schema for an object with title (text) and at (a date).
```

`Effect.catch` is another example. In Effect 4 it catches every failure. In Effect 3 it takes a discriminator and catches one kind of failure. Pinning to one major also drops the other major's names entirely, so a v3-only API in a v4 codebase is narrated generically instead of being explained with the wrong meaning.

## Writing a versioned plugin

Resolve the version inside your rules, because the engine only knows the file's package once narration starts:

```ts theme={null}
export const myLib = ({ version }: { version?: 1 | 2 } = {}) => {
  const versionOf = (e: Engine): 1 | 2 | "auto" => {
    if (version) return version;
    const major = e.libraryMajor("my-lib");
    return major === 1 || major === 2 ? major : "auto";
  };
  return definePlugin({
    name: "my-lib",
    library: { name: "my-lib", versions: ">=1 <3" },
    phrases: { en },
    calls: {
      "*.fetchAll": ({ e, say, obj }) => {
        if (!obj || !e.usesLibrary("my-lib")) return null;
        const phrase = versionOf(e) === 1 ? say.firstPage : versionOf(e) === 2 ? say.everyPage : say.somePages;
        return phrase({ source: e.text(obj) });
      },
    },
  });
};
```

The Effect plugin keeps its rules in groups and merges one table per mode up front, then picks a table per file. You can copy the pattern:

```ts theme={null}
type Versioned = { shared?: OpTable; v3?: OpTable; v4?: OpTable; auto?: OpTable };

function opsFor(version: 3 | 4 | "auto", groups: Versioned[]): OpTable {
  const out: OpTable = {};
  for (const g of groups) {
    Object.assign(out, g.shared);
    if (version !== 4) Object.assign(out, g.v3);
    if (version !== 3) Object.assign(out, g.v4);
    if (version === "auto") Object.assign(out, g.auto);
  }
  return out;
}
```

Test each mode. The repository's Effect tests narrate the same snippet with `version: 3`, `version: 4` and no version, and assert each output. The Elysia tests do the same with versions 1 and 2.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.