Two builds exist side by side now: cargo’s and Vite’s. This lesson makes
one import statement — import 'chess-engine' — mean the right thing in
dev, in production, and (two lessons early but deliberately) in tests. The
payoff is an ASCII chessboard printed by Rust into a browser page.
Checkpoint:
lesson-05.
The alias trio
chess-engine/pkg/ is not an npm package in node_modules; it’s a build
artifact sitting in the repo. Three configuration entries, in two files,
make it importable by its bare name — and all three are needed:
// vite.config.ts
import wasm from 'vite-plugin-wasm';
export default defineConfig({
plugins: [wasm()],
build: { target: 'esnext' },
resolve: {
alias: {
'chess-engine': path.resolve(import.meta.dirname, 'chess-engine/pkg/chess_engine.js')
}
},
optimizeDeps: { exclude: ['chess-engine'] },
});
// tsconfig.json, inside compilerOptions
"paths": {
"chess-engine": ["./chess-engine/pkg/chess_engine"]
}
The vite alias is the runtime: it points the bare specifier at the
generated glue file. The tsconfig paths entry is the types: it points the
same specifier at the generated .d.ts, so import type { ChessGame }
resolves and your editor autocompletes the whole lesson-4 surface. They
name the same thing in two type systems, which is why they travel together.
The third entry is the one everyone forgets. Vite pre-bundles dependencies
with esbuild for speed, and esbuild would inline the glue file — breaking
the new URL('chess_engine_bg.wasm', import.meta.url) reference it uses to
locate the binary. optimizeDeps.exclude tells Vite to keep its hands off.
build.target: 'esnext' rounds out the set, because the glue and
vite-plugin-wasm produce module patterns older targets can’t express.
Initialization is explicit
A wasm module doesn’t just import; it must be fetched, compiled, and
instantiated. The generated glue exports that step as the default export.
Replace src/main.ts with a temporary smoke test:
// Temporary smoke test: prove the engine reaches the browser. The board
// component replaces all of this in the next lesson.
import init, { ChessGame } from 'chess-engine';
await init();
const game = new ChessGame(true);
console.log('Engine loaded. FEN:', game.get_fen());
document.body.insertAdjacentHTML('beforeend', `<pre>${game.get_ascii_board()}</pre>`);
init() is where the browser fetch()es the .wasm file. Everything
before it resolves is a module that owns no memory; call a method too early
and you get a hard-to-read error about null pointers. The top-level await
compiles because of the esnext target you just set.
Run the dev server and there it is: shakmaty’s debug rendering of the
starting position, in a <pre> tag, put there by Rust. Run
npm run build and check the output listing — the .wasm appears as a
hashed asset next to your JavaScript, which is the whole story of wasm in
production: it’s a static file.
Where the seams are
Nothing was configured for tests yet, but notice what this setup already
implies. The glue’s init() guards itself with an “already initialized?”
check, and it has a synchronous sibling, initSync(). Lesson 11 will
exploit that pair to run this exact component in Node, where fetch()
doesn’t serve local files. Knowing the seam exists is enough for today.
Build it
| File | Action | What goes in it |
|---|---|---|
vite.config.ts |
modify | The wasm plugin, build.target, the alias, the optimizeDeps exclusion |
tsconfig.json |
modify | The paths entry |
package.json |
modify | Add vite-plugin-wasm to devDependencies |
src/main.ts |
write | The smoke test above |
Done when: the dev server shows an ASCII board and logs the starting
FEN, and npm run build lists a chess_engine_bg-*.wasm asset.
Answer key: lesson-04...lesson-05.
Challenge: break it on purpose
Remove optimizeDeps.exclude, hard-refresh the dev server, and read the
failure you get. Then restore it. Configuration you’ve watched fail is
configuration you can debug at 11 pm; this one line produces one of the
more confusing error messages in the pipeline.
Next lesson, pixels: a Lit component turns get_fen() into 64 squares, and
the ASCII board retires.