Get startedJavaScript and WebAssembly

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.

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

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

ImportResolves toUse it when
texformThe Node loader under Node.js, the bundler loader under a bundlerAlmost always
texform/nodeThe Node loader, unconditionallyConditional resolution picks the wrong one, or a tool reads the package without Node conditions
texform/bundlerThe bundler loader, unconditionallyYou 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.

JavaScript
import { TransformEngine } from 'texform';

const engine = new TransformEngine({ profile: 'corpus' });
console.log(engine.normalize('a \\over b'));
JavaScript
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.

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

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#

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.

JavaScript
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:

TypeScript
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 — 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#

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.

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

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#

Last updated