Most Cargo.toml files are a dependency list. This one is an architecture document, and it’s short enough to read every line. This lesson creates the chess-engine crate, builds it to WebAssembly for the first time, and runs its first native tests — the same code, two targets, zero conditional compilation. Checkpoint: lesson-02.

The manifest, line by line

Create chess-engine/ inside the project (a plain subdirectory, not a workspace member of anything) with this manifest:

[package]
name = "chess-engine"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.127"
shakmaty = "0.30.1"
console_error_panic_hook = "0.1.7"

[profile.release]
lto = true
opt-level = 3

The load-bearing line is crate-type. cdylib is what wasm-pack compiles into the .wasm binary; rlib is an ordinary Rust library target, and its entire job here is to make cargo test work on your machine, no browser or wasm tooling involved. Delete "rlib" and the crate still builds for the web, but every test in this course dies. That one word is the testing strategy.

shakmaty is the chess library: board representation, move generation, legality, the works. We are not writing a chess engine from scratch, on purpose — hand-rolled move generation is its own multi-month course, and what this course teaches is the pipeline around it.

The release profile turns on lto and full optimization because the output is a file browsers download. Worth knowing what’s absent, too: no serde (the boundary will be strings), no getrandom (lesson 8 dodges randomness in a way you’ll want to see), no wee_alloc (the default allocator is fine).

The first slice of ChessGame

src/lib.rs starts with a struct and four read-only methods:

#[wasm_bindgen]
pub struct ChessGame {
    board: Chess,
    player_color: Color,
}

#[wasm_bindgen]
impl ChessGame {
    #[wasm_bindgen(constructor)]
    pub fn new(player_white: bool) -> Self {
        Self {
            board: Chess::new(),
            player_color: if player_white { Color::White } else { Color::Black },
        }
    }
}

#[wasm_bindgen(constructor)] means JavaScript will call new ChessGame(true). Alongside it, add get_fen() (serialize the whole position with Fen::from_position), get_turn(), is_human_turn(), and get_ascii_board() — the full bodies are small and in the answer key. Then the module’s only free function:

#[wasm_bindgen(start)]
pub fn init() {
    console_error_panic_hook::set_once();
}

start runs automatically when the wasm module initializes. Without this hook, a Rust panic in the browser reads as RuntimeError: unreachable; with it, you get the actual panic message in the console. It’s three lines that will save you an evening.

First build, first tests

npx wasm-pack build --target web --out-dir pkg

Wire that into package.json as build:wasm (with wasm-pack added as a dev dependency, so nobody needs a global install), plus test:rust for the native side. The build emits chess-engine/pkg/: the .wasm binary, a JavaScript glue file, and a generated .d.ts — look inside it and find your constructor already typed.

wasm-pack also drops a .gitignore containing * into pkg/, assuming you’ll publish to npm. We do the opposite: delete that file and commit pkg/, so anyone cloning the repo can run the app with no Rust toolchain installed. The price is remembering to rebuild after touching Rust.

The first tests go in a #[cfg(test)] mod tests at the bottom of lib.rs: a new game sits at the standard starting FEN with white to move, and a player who chose black is not on turn. They run natively, in milliseconds, because of that rlib.

Build it

File Action What goes in it
chess-engine/Cargo.toml write The manifest above, exactly
chess-engine/src/lib.rs write ChessGame struct, constructor, the four getters, the panic-hook start fn, three tests
chess-engine/pkg/ generated npm run build:wasm output, .gitignore inside it deleted, committed
package.json modify Add build:wasm and test:rust scripts, wasm-pack devDependency

Done when: npm run test:rust reports 3 passed, and npm run build:wasm leaves a chess_engine_bg.wasm in pkg/.

Answer key: lesson-01...lesson-02.

Challenge: weigh the binary

Build once as-is, then comment out the [profile.release] block and build again, comparing the size of chess_engine_bg.wasm. Then try opt-level = "s". Write down which knob moved the number most — for a 717 KB binary shipped to every visitor, this table is worth having.

Next lesson the crate learns to actually play: moves in two notations, promotion that defaults sensibly, and the parsing trick that makes both work through one method.