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:
wasm-parity.ts
Output
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.
dependencies index is a fourth.
narrate-file.ts
Output
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:
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.
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.
src/narrate.ts
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:
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.
src/worker.ts
wrangler.jsonc
nodejs_compat flag is needed. Posting a small Hono app to it under wrangler dev gives back:
--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.
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 does in the worker where it narrates:
'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: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.
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 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, usingline.dfor indentation andline.kindfor styling. See Markup.
narrator.diffFiles returns structured units with their Lines, which renderDiffText prints as text, and narrator.diff returns HTML-ready rows. See Units and diffs.