> ## 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.

# Embedding in your app

> Run Narrator on a server with the native parser, or in a browser, a worker or an edge runtime with the WASM parser.

The core package has no runtime dependencies, and the engine is plain TypeScript. The only platform-specific part is the parser, and you choose it when you create the TypeScript source language.

| Where you run | Parser | Import |
| - | - | - |
| Node, Bun, Deno, CI, a CLI | Native oxc binding | `oxcParser` from `@usenarrator/lang-ts/oxc` |
| Browsers, Web Workers, extension service workers, Cloudflare Workers and other edge runtimes | oxc compiled to WASM | `createWasmParser` from `@usenarrator/lang-ts/wasm` |

The WASM parser is oxc's own WebAssembly build, `@oxc-parser/binding-wasm32-wasip1`, which oxc publishes alongside every release of the native `oxc-parser`. `@usenarrator/lang-ts` ships it inside the package, together with the runtime it needs, so there is nothing extra to install. With the matching version of `oxc-parser` on the server, both parsers produce the same tree, so the English is identical. We check this on every change by parsing and narrating 15,820 real files with both parsers and comparing the trees and the output byte for byte. This script does the same for one snippet:

```ts wasm-parity.ts theme={null}
import { createNarrator, renderText } from "@usenarrator/core";
import { typescript } from "@usenarrator/lang-ts";
import { oxcParser } from "@usenarrator/lang-ts/oxc";
import { createWasmParser } from "@usenarrator/lang-ts/wasm";
import { en } from "@usenarrator/locale-en";

const source = `export const total = (items: Item[]) => items.reduce((sum, item) => sum + item.price * item.qty, 0);`;

const native = createNarrator({ languages: [typescript({ parser: oxcParser })], locale: en });
const wasm = createNarrator({ languages: [typescript({ parser: await createWasmParser() })], locale: en });

const a = renderText(native.narrate(source, "cart.ts").lines);
const b = renderText(wasm.narrate(source, "cart.ts").lines);
console.log(a);
console.log(a === b ? "native and WASM agree" : "native and WASM differ");
```

```text Output theme={null}
To total, given items (a list of Item):
  Give back the total of each item's price times each item's quantity across items.
native and WASM agree
```

## On a server

On Node and Bun, the quickest setup is `@usenarrator/node`. `createNodeNarrator` wires the TypeScript language to the native parser, uses English unless you pass another locale, and loads the dependency manifests of the git checkout around the working directory so plugins know what each package uses.

```ts theme={null}
import { createNodeNarrator, loadDependencyIndex } from "@usenarrator/node";
import { hono } from "@usenarrator/plugin-hono";

// Manifests from the repository around process.cwd():
export const narrator = createNodeNarrator({ plugins: [hono()] });

// Or point it at another checkout, or turn manifest reading off:
const other = createNodeNarrator({ plugins: [hono()], dependencies: loadDependencyIndex({ root: "/srv/repos/api" }) });
const bare = createNodeNarrator({ dependencies: false });
```

To choose every piece yourself, build the narrator from its parts. A narrator is three choices: which source languages it reads, which plugins those languages use, and which locale it writes in. The optional `dependencies` index is a fourth.

```ts narrate-file.ts theme={null}
import { createNarrator, renderText } from "@usenarrator/core";
import { typescript } from "@usenarrator/lang-ts";
import { oxcParser } from "@usenarrator/lang-ts/oxc";
import { en } from "@usenarrator/locale-en";
import { zod } from "@usenarrator/plugin-zod";

const narrator = createNarrator({
	languages: [typescript({ parser: oxcParser, plugins: [zod()] })],
	locale: en,
});

const source = `
export const Signup = z.object({ email: z.string().email(), plan: z.enum(["free", "pro"]) });

export function countOpenSeats(team: Team) {
  if (team.plan === "enterprise") return Infinity;
  return team.seatLimit - team.members.length;
}
`;

const { lines } = narrator.narrate(source, "signup.ts");
console.log(renderText(lines));
```

```text Output theme={null}
Let signup be a schema for an object with email and plan.

To count open seats, given team (Team):
  If team's plan is "enterprise", give back infinity.
  Give back team's seat limit minus the number of team's members.
```

A narrator is cheap to create and safe to reuse. It creates a fresh file narrator for each file, so per-file state never leaks between calls. File paths matter: plugins such as Next.js read routes from them, and the dependency index looks up the nearest `package.json` by path, so pass repository-relative paths where you can.

## In a browser, a worker or at the edge

Install the Narrator packages. The WASM parser is part of `@usenarrator/lang-ts`, so you don't need anything else:

```sh theme={null}
npm install @usenarrator/core @usenarrator/lang-ts @usenarrator/locale-en
```

Then load it once with `createWasmParser`. It returns a promise for an ordinary parser, so after that first `await` every call to `narrate` is synchronous, exactly like on a server.

```ts theme={null}
import { createWasmParser } from "@usenarrator/lang-ts/wasm";

const parser = await createWasmParser();
```

Called without options, `createWasmParser` finds the `.wasm` file by itself in Node, Bun, and bundlers that turn `new URL("…", import.meta.url)` into an asset, such as Vite, Rollup, esbuild with its file loader, and webpack. Everywhere else, pass the file with the `wasm` option, as a URL, a `Response`, its bytes or an already compiled `WebAssembly.Module`. Each call starts a new WASM instance, so create the parser once and share it.

The recipes below were each built and run from the packed packages in fresh projects outside this repository, installed with npm, pnpm and Bun without any extra configuration. The timings were taken on a Linux x64 cloud machine, so treat them as a rough guide rather than a promise.

### Vite

Vite needs no configuration. It sees the `.wasm` file that `@usenarrator/lang-ts/wasm` references and copies it into your build, in both `vite dev` and production builds.

```ts src/narrate.ts theme={null}
import { createNarrator, renderText } from "@usenarrator/core";
import { typescript } from "@usenarrator/lang-ts";
import { createWasmParser } from "@usenarrator/lang-ts/wasm";
import { en } from "@usenarrator/locale-en";
import { zod } from "@usenarrator/plugin-zod";

const narrator = createNarrator({
  languages: [typescript({ parser: await createWasmParser(), plugins: [zod()] })],
  locale: en,
});

export const narrate = (source: string, path = "input.ts") => renderText(narrator.narrate(source, path).lines);
```

Import this module with `await import("./narrate")` from your entry point, so the page can render before the parser arrives. With Vite 8 and headless Chrome, the first narration appeared 58 to 73 ms after navigation in a production build served from localhost with the cache disabled, which includes loading the page and downloading, compiling and starting the WASM. In `vite dev` it took about 70 ms once Vite had transformed the modules. After that, narrating a few lines on every keystroke took about 2 ms.

The production build contains these files:

| File | Size | Gzipped |
| - | - | - |
| The parser's `.wasm` | 1,456 KB | 485 KB |
| Engine, English locale, the Zod plugin, and oxc's WASM runtime and loader | 397 KB | 105 KB |

### Cloudflare Workers

Workers can't compile WebAssembly while they run, so import the `.wasm` file as a module. Wrangler compiles it when you deploy and gives your code a `WebAssembly.Module`. `@usenarrator/lang-ts/wasm.wasm` points at the file the package ships, and its name ends in `.wasm` so that Wrangler's rule for `.wasm` imports applies.

```ts src/worker.ts theme={null}
import wasm from "@usenarrator/lang-ts/wasm.wasm";
import { createNarrator, renderText } from "@usenarrator/core";
import { typescript } from "@usenarrator/lang-ts";
import { createWasmParser } from "@usenarrator/lang-ts/wasm";
import { en } from "@usenarrator/locale-en";
import { hono } from "@usenarrator/plugin-hono";

const narrator = createWasmParser({ wasm }).then((parser) =>
  createNarrator({ languages: [typescript({ parser, plugins: [hono()] })], locale: en }),
);

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method !== "POST") return new Response("POST a TypeScript file\n", { status: 405 });
    const path = new URL(request.url).searchParams.get("path") ?? "input.ts";
    const { lines } = (await narrator).narrate(await request.text(), path);
    return new Response(`${renderText(lines)}\n`);
  },
};
```

```jsonc wrangler.jsonc theme={null}
{
  "name": "narrator-worker",
  "main": "src/worker.ts",
  "compatibility_date": "2026-10-01"
}
```

No `nodejs_compat` flag is needed. Posting a small Hono app to it under `wrangler dev` gives back:

```text theme={null}
Create a Hono app as app.
Answer GET /health with an object with ok: true as JSON.
```

The first request to a fresh isolate took 14 to 15 ms, including starting the WASM, and later requests took 3 to 20 ms. Wrangler reports an upload of 2,269 KiB, or 626 KiB gzipped, and 1,870 KiB, or 593 KiB gzipped, with `--minify`. That fits the 3 MB limit of the Workers free plan.

### Node and Bun

On a server you would normally use the native parser, but the WASM parser also runs in Node and Bun, which helps on platforms where native modules aren't allowed. No options are needed: it reads the `.wasm` file that ships inside `@usenarrator/lang-ts`.

```ts theme={null}
import { createNarrator, renderText } from "@usenarrator/core";
import { typescript } from "@usenarrator/lang-ts";
import { createWasmParser } from "@usenarrator/lang-ts/wasm";
import { en } from "@usenarrator/locale-en";

const narrator = createNarrator({ languages: [typescript({ parser: await createWasmParser() })], locale: en });
console.log(renderText(narrator.narrate("export const isAdmin = (user: User) => user.role === 'admin';", "auth.ts").lines));
```

On Node 24 and Bun 1.3, importing the packages took 7 to 10 ms, starting the WASM took 27 to 30 ms, and the first narration took 5 to 11 ms. The whole process finished in 0.05 to 0.06 seconds. Narration with the WASM parser takes between 0.9 and 1.7 times as long as with the native one.

### Browser extensions

Copy the `.wasm` file into the extension when you build it, and pass its extension URL. The file is `dist/oxc-parser.wasm` inside `@usenarrator/lang-ts`, and you can find it with `require.resolve("@usenarrator/lang-ts/wasm.wasm")` or `import.meta.resolve` in your build script. This is what the [browser extension](/guides/browser-extension) does in the worker where it narrates:

```ts theme={null}
const parser = await createWasmParser({ wasm: chrome.runtime.getURL("oxc-parser.wasm") });
```

Manifest V3 extensions also need `'wasm-unsafe-eval'` in their `content_security_policy.extension_pages`.

### When something is wired wrong

Narrator checks the common mistakes and says what to do instead of failing deep inside the parser:

| Mistake | Error |
| - | - |
| The server answers with your app's HTML instead of the `.wasm` file | The URL "is not a WebAssembly file (it starts with HTML, so the server probably answered with your app's index page)". |
| `wasm` is something else, such as a number or a module namespace object | "`wasm` must be a URL, a Response, the file's bytes or a WebAssembly.Module", or "Pass its default export". |
| `typescript({ parser: createWasmParser() })` without `await` | "got a Promise; await createWasmParser() before creating the narrator". |
| `typescript({ parser: createWasmParser })`, the function itself | "returned a Promise; pass the result of `await createWasmParser()`". |
| Node or Bun can't find `oxc-parser.wasm`, for example after bundling Narrator into one file | Names the path it tried and asks you to copy the file next to the bundle or pass `wasm`. |

<Tip>
  Narration is synchronous and usually takes a few milliseconds per file, but a large pull request can take longer. In a web app, run it in a Web Worker so the page stays responsive. `createWasmParser` works the same way inside a worker.
</Tip>

## Bundle size

These numbers come from the minified extension build, which splits the code into chunks and loads each one only when it is needed.

| Piece | Minified | Gzipped |
| - | - | - |
| oxc parser WASM | 1,422 KB | 473 KB |
| oxc's WASM runtime and loader | 238 KB | 58 KB |
| One plugin | usually 10 to 45 KB | usually 3 to 14 KB |
| The `pgsql-ast-parser` used by the SQL plugin | 249 KB | 43 KB |
| Everything except the SQL parser: engine, English locale and every plugin the extension ships | 973 KB | 326 KB |

The WASM file is the largest part, and it is fetched once and cached by the browser. The JavaScript grows with each plugin you include, so load plugins lazily with `import()` and only for the libraries a change uses, the way the [browser extension](/guides/browser-extension#how-it-stays-fast) does.

## Rendering the output

`narrate` gives you `Line` objects. Pick the renderer that suits your UI:

* `renderText(lines)` for plain text, with two spaces of indent per level.
* `renderText(lines, toAnsi)` for a terminal.
* `toHtml(line.text, resolve)` per line for a web page, using `line.d` for indentation and `line.kind` for styling. See [Markup](/concepts/markup).

For diffs, `narrator.diffFiles` returns structured units with their `Line`s, which `renderDiffText` prints as text, and `narrator.diff` returns HTML-ready rows. See [Units and diffs](/concepts/units-and-diffs).


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