# JavaScript and WebAssembly

Pick the right texform entry point for Node or a bundler, configure top-level await and .wasm assets, and manage handle lifetimes.

https://texform.dev/docs/javascript-and-wasm

The npm `texform` package is the Rust core compiled to WebAssembly, wrapped in a small JavaScript layer that adds structured errors and camelCase names. Everything in [Quick start](https://texform.dev/docs/quick-start) works unchanged in JavaScript; this page covers the two things that are specific to a WASM package — getting the module loaded, and knowing when a handle is released.

## Three entry points [#three-entry-points]

The package declares three specifiers, and they resolve to two different loaders:

| Import            | Resolves to                                                       | Use it when                                                                                     |
| ----------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `texform`         | The Node loader under Node.js, the bundler loader under a bundler | Almost always                                                                                   |
| `texform/node`    | The Node loader, unconditionally                                  | Conditional resolution picks the wrong one, or a tool reads the package without Node conditions |
| `texform/bundler` | The bundler loader, unconditionally                               | You bundle for the browser but the tool reports a Node environment                              |

The bare `texform` specifier is the one to reach for. It maps to `./node/index.js` (or `./node/index.cjs` for `require`) under Node's conditions, and to `./bundler/index.js` for `browser` and default conditions. The two subpath specifiers exist to override that decision, not to be the normal choice — all three ship the same TypeScript declarations and the same API.

**Automatic**

```js
import { TransformEngine } from 'texform';

const engine = new TransformEngine({ profile: 'corpus' });
console.log(engine.normalize('a \\over b'));
```

**Node**

```js
import { TransformEngine } from 'texform/node';

const engine = new TransformEngine({ profile: 'corpus' });
console.log(engine.normalize('a \\over b'));
```

The Node loader requires the `.wasm` module through `createRequire`, so it is fully synchronous: by the time the `import` statement resolves, the engine is ready. CommonJS works too — `require('texform')` resolves to the CommonJS build of the same wrapper.

**Bundler**

```js
import { TransformEngine } from 'texform/bundler';

const engine = new TransformEngine({ profile: 'corpus' });
console.log(engine.normalize('a \\over b'));
```

The bundler loader awaits WebAssembly instantiation at module scope, so importing it is enough — there is no `init()` for you to call and no readiness flag to check.

## What the bundler entry needs [#what-the-bundler-entry-needs]

The bundler loader resolves the `.wasm` file as an asset URL and then awaits its instantiation at the top level of the module. That puts two requirements on your build:

* **Top-level `await` must survive bundling.** The loader is an ES module with a top-level `await`, which means the output format has to be ESM and the target has to be modern. Vite and webpack 5 handle this; a build configured to emit CommonJS or to target older browsers will fail here, usually with an error naming top-level `await` rather than TeXForm.
* **`.wasm` must be emitted as an asset.** The module is fetched at runtime from a URL, not inlined into the JavaScript bundle. A bundler that does not know how to emit `.wasm` will either drop the file or try to parse it as JavaScript.

Because the import is awaited, the first module in your graph that imports `texform` becomes async. That is normally invisible, but it does mean a synchronous entry point cannot import TeXForm at the top level — move the import into an async boundary, such as a dynamic `import()` inside the handler that needs it.

## Handle lifetimes [#handle-lifetimes]

`Parser`, `TransformEngine`, `Document`, and `Node` are handles to objects that live in WebAssembly memory, not plain JavaScript values. Each one exposes `free()` and `[Symbol.dispose]()` to release the underlying object.

```js
import { TransformEngine } from 'texform';

const formulas = ['a \\over b', '{\\bf x}'];
const engine = new TransformEngine({ profile: 'corpus' });
try {
  for (const formula of formulas) {
    console.log(engine.normalize(formula));
  }
} finally {
  engine.free();
}
```

With explicit resource management (TypeScript 5.2 or newer, or a runtime that supports `using`), the same thing reads as a declaration:

```ts
import { TransformEngine } from 'texform';

{
  using engine = new TransformEngine({ profile: 'corpus' });
  console.log(engine.normalize('a \\over b'));
}
```

Two habits keep this simple. Build one engine and reuse it across a whole batch rather than constructing one per formula, as in [Normalize a corpus](https://texform.dev/docs/normalize-a-corpus) — that is the right call for performance anyway. And release a `Document` only after you are done with the `Node` handles you obtained from it, since those nodes are references back into that document rather than independent copies.

## Configuration is overlays, not classes [#configuration-is-overlays-not-classes]

The JavaScript API has no configuration classes. Every per-call config is a plain object overlay: you pass only the keys you want to change, and omitted keys (or `null` / `undefined`) keep the value the profile or parser already has.

```js
import { TransformEngine } from 'texform';

const engine = new TransformEngine({ profile: 'corpus' });
engine.normalize('a \\over b', { rewrite: { enabled: false } });
engine.normalize('a \\over b', { rejectUnknown: true, flattenGroups: { enabled: false } });
```

Overlays are validated strictly. An unknown key, a snake_case key, an array where an object belongs, or a wrong scalar type throws `TexformConfigError` with the camelCase path of the offending field. Enum *values* stay snake_case even though keys are camelCase — `{ scriptOrder: 'sub_first' }` is correct.

When you need to know what is actually in force rather than what you overrode, ask: `parser.defaultParseConfig()`, `engine.defaultParseConfig()`, and `engine.defaultTransformConfig()` return the complete resolved configuration.

## Errors [#errors]

Every failure that crosses the WebAssembly boundary arrives as a structured JavaScript exception; no Rust panic reaches your code. The classes are `TexformError` and its subclasses `TexformParseError`, `TexformEditError`, `TexformConfigError`, and `TexformTransformError`. A `TexformParseError` additionally carries `diagnostics` and, when one exists, the partial `document`.

## Next steps [#next-steps]

- [Quick start](https://texform.dev/docs/quick-start) — The core normalize-and-inspect path, with a JavaScript tab at every step.

- [Troubleshooting](https://texform.dev/docs/troubleshooting) — What to check when the module will not load or a config is rejected.

- [Serialization](https://texform.dev/docs/serialization) — The output channels and the options that shape the LaTeX string.

