# WASM and playground

> Build wakaru-wasm for the browser playground, decompile/unpack JS bindings, TypeScript result types, and Vite integration for the online demo.

- Repository: pionxzh/wakaru
- GitHub: https://github.com/pionxzh/wakaru
- Human docs: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b
- Complete Markdown: https://grok-wiki.com/public/docs/pionxzh-wakaru-77a438a6cc6b/llms-full.txt

## Source Files

- `crates/wasm/src/lib.rs`
- `playground/package.json`
- `playground/scripts/build-wasm.mjs`
- `playground/vite.config.ts`
- `.github/workflows/playground.yml`

---

---
title: "WASM and playground"
description: "Build wakaru-wasm for the browser playground, decompile/unpack JS bindings, TypeScript result types, and Vite integration for the online demo."
---

The `wakaru-wasm` crate (`crates/wasm`) compiles `wakaru-core` to WebAssembly via `wasm-bindgen` and exposes `decompile`, `unpack`, and `ruleNames` to JavaScript. The React playground in `playground/` loads the generated `crates/wasm/pkg` bundle through a Vite alias, runs decompilation in a dedicated Web Worker, and deploys to Vercel at `/playground/`.

## Architecture

```mermaid
flowchart TB
  subgraph browser["Browser (playground)"]
    UI["React UI (App.tsx)"]
    Bridge["WasmBridge"]
    Worker["Web Worker (worker.ts)"]
    UI --> Bridge
    Bridge <-->|postMessage| Worker
  end

  subgraph wasm_pkg["crates/wasm/pkg (wasm-pack output)"]
    Init["default init()"]
    Decompile["decompile()"]
    Unpack["unpack()"]
    RuleNames["ruleNames()"]
  end

  subgraph rust["Rust workspace"]
    Core["wakaru-core"]
    Formatter["wakaru-formatter"]
    Lib["crates/wasm/src/lib.rs"]
    Lib --> Core
    Lib --> Formatter
  end

  Worker --> Init
  Worker --> Decompile
  wasm_pkg -.-> Lib
```

The playground UI calls only `decompile` today. `unpack` and `ruleNames` are exported for programmatic use and match the CLI/core semantics.

| Layer | Path | Role |
|---|---|---|
| WASM crate | `crates/wasm/` | `cdylib` bindings, serde result types, embedded TypeScript defs |
| Generated JS/WASM | `crates/wasm/pkg/` | `wasm-pack` output (gitignored) |
| Build script | `playground/scripts/build-wasm.mjs` | Invokes `wasm-pack` before Vite |
| Playground app | `playground/src/` | Monaco editors, controls, share URLs, source-map overlay |
| Worker bridge | `playground/src/wasm/` | Off-main-thread WASM init and request routing |

## Build wakaru-wasm

### Prerequisites

- Rust stable with `wasm32-unknown-unknown` target
- [`wasm-pack`](https://rustwasm.github.io/wasm-pack/)
- Node.js (playground uses Node 24 in CI)

<Steps>
<Step title="Install the WASM target">

```bash
rustup target add wasm32-unknown-unknown
```

</Step>
<Step title="Build the package">

From the repository root:

```bash
wasm-pack build crates/wasm --target web --out-dir pkg --release
```

Or from `playground/`:

```bash
npm run build:wasm
```

`build-wasm.mjs` runs the same `wasm-pack` invocation with `--target web` and writes to `crates/wasm/pkg`.

</Step>
<Step title="Verify output">

After a successful build, `crates/wasm/pkg/` contains at least:

- `wakaru_wasm.js` — ES module glue code
- `wakaru_wasm_bg.wasm` — compiled binary
- `wakaru_wasm.d.ts` — generated types (supplemented by the crate's `typescript_custom_section`)

</Step>
</Steps>

<Note>
`crates/wasm/Cargo.toml` sets `wasm-opt = false` for the release profile. Binary size is traded for faster CI builds and predictable output.
</Note>

### Crate configuration

`wakaru-wasm` is a workspace member packaged as `cdylib`. Dependencies include `wakaru-core`, `wakaru-formatter`, `wasm-bindgen`, `serde-wasm-bindgen`, `console_error_panic_hook`, and `getrandom` with the `wasm_js` feature for browser entropy.

The `#[wasm_bindgen(start)]` `init` function installs `console_error_panic_hook` so Rust panics surface in the browser console.

## JavaScript bindings

Three functions are exported from `crates/wasm/src/lib.rs`. All return structured objects serialized through `serde_wasm_bindgen`. Errors from `wakaru-core` propagate as thrown `JsValue` strings.

### `decompile`

Single-file decompilation with optional source-map input and emitted output map.

<ParamField body="source" type="string" required>
Input JavaScript source.
</ParamField>

<ParamField body="level" type='"minimal" | "standard" | "aggressive"'>
Rewrite level. Defaults to `standard` when omitted or invalid (via `RewriteLevel::from_str_or_default`).
</ParamField>

<ParamField body="sourcemap" type="Uint8Array">
Raw bytes of a v3 source map for identifier recovery and import deduplication.
</ParamField>

<ParamField body="diagnostics" type="boolean">
When `true`, runs post-transform checks (TDZ, output parse). Default: `false`.
</ParamField>

<ParamField body="formatter" type="boolean">
When `true`, runs the Oxc formatter after decompilation. Default: `false` (no formatting).
</ParamField>

<ParamField body="emitSourceMap" type="boolean">
When `true`, returns a v3 source map mapping output back to input. Default: `false`.
</ParamField>

<ResponseField name="code" type="string">
Decompiled (and optionally formatted) JavaScript.
</ResponseField>

<ResponseField name="source_map" type="string">
Emitted source map JSON. Omitted when `emitSourceMap` is `false` or no map was produced.
</ResponseField>

<ResponseField name="warnings" type="WakaruWarning[]">
Non-fatal pipeline and formatter warnings.
</ResponseField>

WASM `decompile` sets `dce_mode: DceMode::TransformOnly` (unlike the CLI default of `DceMode::Off`) and hardcodes `filename: "input.js"`.

<RequestExample>

```javascript title="Decompile in the browser"
import init, { decompile } from "wakaru-wasm";

await init();
const result = decompile(
  "var n=function(){return 1};",
  "standard",
  undefined,
  true,
  true,
  true
);
console.log(result.code);
console.log(result.warnings);
```

</RequestExample>

### `unpack`

Bundle unpacking for browser-side automation. Mirrors `wakaru_core::unpack` with per-module formatting.

<ParamField body="source" type="string" required>
Bundle JavaScript source.
</ParamField>

<ParamField body="level" type='"minimal" | "standard" | "aggressive"'>
Rewrite level.
</ParamField>

<ParamField body="heuristicSplit" type="boolean">
Enables scope-hoisted heuristic splitting when no structural bundle is detected. Default: `true`.
</ParamField>

<ParamField body="diagnostics" type="boolean">
Post-transform diagnostic checks. Default: `false`.
</ParamField>

<ParamField body="formatter" type="boolean">
Oxc formatting per extracted module. Default: `false`.
</ParamField>

<ParamField body="emitSourceMap" type="boolean">
Per-module emitted source maps. Default: `false`.
</ParamField>

<ResponseField name="modules" type="WakaruModule[]">
Extracted modules with `filename` and `code`.
</ResponseField>

<ResponseField name="source_maps" type="WakaruSourceMap[]">
Per-module maps (`filename`, `map`). Omitted when empty.
</ResponseField>

<ResponseField name="warnings" type="WakaruWarning[]">
Unpack and formatter warnings.
</ResponseField>

### `ruleNames`

Returns the ordered pipeline rule identifiers from `wakaru_core::rule_names()` as a `string[]`. Useful for debugging and UI that lists active rules.

## TypeScript result types

Types are declared in two places:

1. **Embedded section** — `#[wasm_bindgen(typescript_custom_section)]` in `lib.rs` augments `wakaru_wasm.d.ts` with canonical interfaces and camelCase parameter names on exported functions.
2. **Playground shim** — `playground/src/vite-env.d.ts` declares the `wakaru-wasm` module for the Vite alias, including `default init()` and snake_case fields on result objects (`source_map` on `WakaruDecompileResult`).

### Core interfaces

```typescript title="Embedded TypeScript definitions (lib.rs)"
export interface WakaruDecompileResult {
    code: string;
    source_map?: string;
    warnings: WakaruWarning[];
}

export interface WakaruUnpackResult {
    modules: WakaruModule[];
    source_maps?: WakaruSourceMap[];
    warnings: WakaruWarning[];
}

export type WakaruWarningKind =
    | "raw_normalization_failed"
    | "fact_collection_parse_failed"
    | "decompile_failed"
    | "tdz_violation"
    | "output_parse_failed"
    | "formatter_failed";
```

Formatter failures add warnings with `kind: "formatter_failed"` and a message naming the formatter (`oxc`) and underlying error.

<Warning>
Serde field names use snake_case (`source_map`, `source_maps`). The playground worker maps `result.source_map` to `sourceMap` in its internal bridge types. When consuming `wakaru-wasm` directly, read `source_map` from the returned object.
</Warning>

## Vite integration

`playground/vite.config.ts` wires the WASM artifact into the React app.

| Setting | Value | Purpose |
|---|---|---|
| `resolve.alias["wakaru-wasm"]` | `../crates/wasm/pkg` | Import the local wasm-pack output |
| `plugins` | `react()`, `wasm()` | `vite-plugin-wasm` enables `.wasm` imports |
| `worker.plugins` | `[wasm()]` | WASM support inside Web Workers |
| `optimizeDeps.exclude` | `["wakaru-wasm"]` | Skip pre-bundling the native WASM module |
| `server.fs.allow` | Repository root | Dev server can read `crates/wasm/pkg` |
| `build.target` | `esnext` | Modern JS for worker modules |
| `base` | `/playground/` (build) / `/` (dev) | Production assets served under `/playground/` |

Build-time defines inject version metadata from the workspace `Cargo.toml` and current git hash:

- `import.meta.env.VITE_WAKARU_VERSION`
- `import.meta.env.VITE_WAKARU_GIT_HASH`

The header displays `v{version}` and links to the commit on GitHub.

### npm scripts

```json title="playground/package.json scripts"
"build:wasm": "node scripts/build-wasm.mjs",
"dev": "vite",
"build": "npm run build:wasm && vite build",
"preview": "vite preview",
"test": "vitest run"
```

Production `npm run build` always rebuilds WASM before the Vite bundle.

## Playground runtime

### Web Worker isolation

`WasmBridge` spawns a module worker at `playground/src/wasm/worker.ts`. The worker:

1. Handles `{ type: "init" }` by calling `await init()` from `wakaru-wasm`.
2. Handles `{ type: "decompile", ... }` by calling `decompile()` and posting results back with a request `id`.

This keeps SWC/WASM work off the UI thread. Monaco editors and source-map hover overlays stay responsive during decompilation.

### Auto-run behavior

After WASM init, `App.tsx` debounces input changes and re-runs decompilation automatically. Delay scales with the last run duration (60–300 ms). The playground passes `diagnostics: true`, `formatter` from the controls toggle, and `emitSourceMap: true` to power the mapping overlay.

### Controls and features

| Control | WASM parameter | Notes |
|---|---|---|
| Level selector | `level` | `minimal`, `standard`, `aggressive` |
| Formatter toggle | `formatter` | Disabled when Mapping overlay is on |
| Mapping toggle | `emitSourceMap` (always on during run) | Visualizes input↔output line links via emitted map |
| Share button | — | Encodes gzip-compressed state in URL hash (`#state=1\|...`) |

Share limits: source max 1,000,000 characters; encoded state max 200,000 characters. Oversized input shows "Input is too large to share".

<Info>
The live demo is linked from the README at `https://wakaru.vercel.app/playground`. Bug report templates accept playground share URLs as reproductions.
</Info>

## Deployment

`.github/workflows/playground.yml` deploys on pushes to `main` that touch `crates/core/**`, `crates/wasm/**`, `playground/**`, or the workflow itself.

<Steps>
<Step title="CI build pipeline">

1. Install Rust + `wasm32-unknown-unknown`
2. Install `wasm-pack`
3. `wasm-pack build crates/wasm --target web --out-dir pkg --release`
4. `npm install` in `playground/`
5. `vercel build --prod` then `vercel deploy --prebuilt --prod`

</Step>
<Step title="Routing">

`playground/vercel.json` rewrites `/playground` and `/playground/*` to the Vite `dist` output. The main `website/` project proxies `/playground` requests to the dedicated Vercel playground project.

</Step>
</Steps>

Concurrency group `playground-deploy` cancels in-progress deploys when new commits land.

## Local development

<Steps>
<Step title="First-time setup">

```bash
rustup target add wasm32-unknown-unknown
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
cd playground && npm install
```

</Step>
<Step title="Run dev server">

```bash
cd playground
npm run build:wasm   # required before first dev session
npm run dev
```

Open the URL Vite prints (dev uses `base: "/"`).

</Step>
<Step title="Production preview">

```bash
cd playground
npm run build
npm run preview
```

</Step>
</Steps>

<Tip>
Rebuild WASM after changing `crates/core` or `crates/wasm`. The dev server does not watch Rust sources.
</Tip>

## WASM vs CLI defaults

| Option | WASM `decompile` | WASM `unpack` | CLI default |
|---|---|---|---|
| `dce_mode` | `TransformOnly` | `Off` (via `..Default::default()`) | `Off` |
| `heuristic_split` | N/A | `true` | `false` |
| `diagnostics` | `false` unless passed | `false` unless passed | CLI flag |
| `formatter` | Oxc when `true` | Oxc per module when `true` | `--formatter` flag |
| `filename` | `"input.js"` | `"input.js"` | From input path |

For full flag surfaces and JSON stdout schemas, see the CLI and core API reference pages.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| `Failed to resolve import "wakaru-wasm"` | Missing `crates/wasm/pkg` | Run `npm run build:wasm` |
| `WASM not initialized` in worker | `init()` not completed | Ensure `WasmBridge.waitForInit()` resolves before decompile |
| Stale playground output after core changes | WASM not rebuilt | Re-run `npm run build:wasm` |
| Dev server cannot read pkg | `server.fs.allow` misconfigured | Keep Vite config allowing repo root |
| Formatter warnings with `formatter_failed` | Oxc could not format output | Output is preserved; warning is non-fatal |
| Share URL fails | Input exceeds size limits | Reduce source or share a gist link instead |

<AccordionGroup>
<Accordion title="Why is unpack not in the playground UI?">
`unpack` is exported and fully wired in the WASM crate, but the current playground only exercises single-file `decompile`. Bundle unpacking remains a CLI workflow (`wakaru --unpack`) or a custom integration calling `unpack()` from JavaScript.
</Accordion>
<Accordion title="Why run WASM in a worker?">
Decompilation is CPU-heavy. The worker boundary prevents blocking React rendering and Monaco input while `wakaru-core` runs inside WASM.
</Accordion>
</AccordionGroup>

## Related pages

<CardGroup>
<Card title="WASM API reference" href="/wasm-api-reference">
Per-export signatures, JSON shapes, and warning kind strings for `decompile`, `unpack`, and `ruleNames`.
</Card>
<Card title="Core API reference" href="/core-api-reference">
`DecompileOptions`, `DceMode`, `RewriteLevel`, and unpack output types that the WASM layer wraps.
</Card>
<Card title="Rewrite levels and assumptions" href="/rewrite-levels-and-assumptions">
What `minimal`, `standard`, and `aggressive` change in the pipeline.
</Card>
<Card title="Use source maps" href="/use-source-maps">
Input source maps for rename recovery and emitted maps from `--emit-source-map` / `emitSourceMap`.
</Card>
<Card title="Overview" href="/overview">
Three-crate workspace layout and the path from input JavaScript to readable output.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`UnpackWarningKind` codes, formatter failures, and bug-report fields.
</Card>
</CardGroup>
