Before writing any Rust, play the thing. Clone the
companion repo, run
npm install && npm run dev, and beat the computer once. Notice what you’re
touching: pieces glide when they move, your game survives a page reload, the
help drawer explains castling without pausing the game. All of it runs from
static files. There is no server thinking about chess anywhere.
This lesson maps the territory and then builds the smallest possible piece
of it: a TypeScript project that compiles and serves a page. Checkpoint:
lesson-01.
Two halves and a narrow bridge
The finished app is one Rust crate and one web component. The crate,
chess-engine, owns everything that can be wrong: the rules, the board
state, the computer opponent. The component, <chess-board>, owns
everything you can see. Between them sits WebAssembly, and the API that
crosses it is deliberately primitive: FEN strings out, moves in algebraic
notation, comma-separated square lists for highlights. No JSON, no serde, no
shared structs.
That narrowness is a feature you’ll feel in every later lesson. The Rust
side stays testable with plain cargo test because it never touches a JS
type. The TypeScript side stays honest because it can’t reach into the
engine and poke — it can only ask.
The other structural decision worth naming now: the interface is one component. No board component owning square components owning piece components. A chessboard renders from a single string in one pass, and splitting it up would only create seams for state to leak through.
An empty page that proves the toolchain
Everything below goes in a fresh directory. The package.json starts with
three scripts and two dev dependencies:
{
"name": "rust-wasm-chess",
"version": "0.1.0",
"description": "A chess game in the browser: a Rust engine compiled to WebAssembly, a Lit web component in front of it",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^7.0.2",
"vite": "^8.2.1"
}
}
The Vite config earns its keep in lesson 5; today it only pins the port:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
host: true
}
});
index.html is the app shell. Vite treats it as the entry point, follows
the module script, and compiles whatever it finds:
<body>
<h1>rust-wasm-chess</h1>
<script type="module" src="/src/main.ts"></script>
</body>
The flags that look wrong on purpose
Copy the full tsconfig.json from the answer key, then actually read it,
because two lines look like mistakes:
"experimentalDecorators": true,
"useDefineForClassFields": false
Lit’s @customElement and @property decorators are the legacy TC39
proposal, and they only work when class fields compile the old way. Flip
either flag and lesson 6’s component will construct but never react to
anything. The rest of the file is strict-mode TypeScript with
moduleResolution: "bundler", which is what lets Vite own the import story.
Build it
| File | Action | What goes in it |
|---|---|---|
package.json |
write | Name, type: "module", the three vite scripts, typescript + vite as devDependencies |
tsconfig.json |
copy | Strict config with the two Lit-required decorator flags |
vite.config.ts |
write | defineConfig with the dev-server port |
index.html |
write | Shell page with a placeholder heading and the module script tag |
src/main.ts |
write | A console.log proving the toolchain compiles TypeScript |
Done when: npm run dev serves the heading on localhost:3000 with
your log line in the console, and npm run build emits a dist/ directory.
Answer key: lesson-01
(first lesson, so the tag tree is the whole answer).
Challenge: the two strange flags
Write down, in one sentence each, what experimentalDecorators and
useDefineForClassFields actually change about the compiled output. The
Lit decorator docs have the
answer. You’ll appreciate having done this when lesson 6 works on the first
try.
Next lesson, the Rust begins: one Cargo.toml with more to say than most,
and a crate that compiles to two different targets without changing a line.