We compiled our TypeScript parser to WASM – Encore Blog
We compiled our TypeScript parser to WASM<br>How we compiled the Rust parser that reads infrastructure out of Encore code so it runs in the browser, and how we use it to reproduce the parser errors people report.
Jul 15, 2026<br>7 Min Read<br>Ivan Cernja
Jul 15, 2026<br>We compiled our TypeScript parser to WASM<br>How we compiled the Rust parser that reads infrastructure out of Encore code so it runs in the browser, and how we use it to reproduce the parser errors people report.
Ivan Cernja<br>7 Min Read
Encore works out what infrastructure an application needs by reading its code. When you declare an API, a database, a Pub/Sub topic, or a cache, the declaration is ordinary TypeScript, and Encore reads the source to find those declarations and the request and response types attached to them, then provisions the infrastructure to match. We do that reading with a parser written in Rust.
Until now that parser only ran on a developer's machine, so reproducing a reported parser bug meant getting hold of the reporter's project. We compiled it to WASM to run in the browser instead, where a bug can be reproduced from a snippet and a shared link. This post covers how we did that, what it took to run a parser that expects a filesystem where there isn't one, and the playground at tsparser.encore.dev it now backs.
What the parser reads
The infrastructure an Encore application uses is declared in its code, and the parser turns that code into the description Encore provisions from: the APIs and the services they live in, along with the infrastructure they use, from SQL databases and Pub/Sub topics to caches, cron jobs, secrets, and object storage.
The parser does more than check syntax, because the contract for an endpoint comes from the actual TypeScript types of its request and response, so it resolves types the way a type checker does, following imports and generics until it knows the concrete shape of the data crossing the wire. It is built on SWC and does its own type resolution rather than calling out to tsc, and it normally runs on your machine as part of the Encore CLI, reading your project off disk.
Encore's parser reads your TypeScript source and derives a description of the application: the APIs and services it defines, and the infrastructure they use.<br>Your TypeScript .ts source files
rust<br>Encore parser
application description<br>http<br>APIs
sql<br>Databases
events<br>Pub/Sub
cron<br>Cron jobs
Encore's parser turns your TypeScript into a description of the app's APIs and infrastructure.
Compiling it to WASM
The parser is a Rust crate, and Rust compiles to WASM, so running it in the browser is largely a matter of building it for a different target and giving JavaScript a way to call it. We added a small crate that wraps the parser and exposes one function, built with wasm-pack for the web target (#2317):
// tsparser/wasm/src/lib.rs<br>#[wasm_bindgen]<br>pub fn parse(files_json: &str) -> String {<br>let all_files: Vec = match serde_json::from_str(files_json) {<br>Ok(f) => f,<br>Err(e) => return error_output(&format!("invalid input JSON: {e}")),<br>};
// Files under node_modules/ are dependencies: registered for module<br>// resolution, but not parsed as user code.<br>let (nm_files, user_files): (Vec, Vec) = all_files<br>.into_iter()<br>.partition(|f| f.name.starts_with("node_modules/"));
run_parse(user_files, nm_files)
You hand parse a JSON array of { name, content } files, and it returns JSON reporting whether parsing succeeded, any errors, and the application metadata it derived. Errors are collected as they happen through a custom emitter that renders SWC's diagnostics to strings, and a panic hook forwards any Rust panic to the browser console, so a crash surfaces as an error message rather than a silent failure.
The parser was written to run on a developer's machine, where it walks directories to find source files, reads tsconfig.json off disk to resolve import paths, and parses SQL migration files out of a folder. None of that is available in the browser, so each of those assumptions had to be replaced or removed.
Leaving the native parts behind
The parser's ties to the operating system live behind a Cargo feature. Everything that needs a filesystem or a subprocess, from directory traversal to node_modules resolution to SQL parsing, is pulled in only when the native feature is on:
# tsparser/Cargo.toml<br>[features]<br>default = ["native"]<br>native = [<br>"dep:pg_query", # SQL query and migration parsing<br>"dep:walkdir", # directory traversal<br>"swc_ecma_loader/node", # node_modules resolution off disk<br># duct, symlink, junction, env_logger, ...<br>serde-meta = []
The WASM crate depends on the parser with that feature switched off, so none of those crates are compiled into the browser build in the first place:
# tsparser/wasm/Cargo.toml<br>[dependencies]<br>encore-tsparser = { path = "..", default-features = false, features = ["serde-meta"] }
With native off, the code that would call into those crates is...