This file provides guidance to coding agents (e.g. Claude Code) working in this repository.
boardgame.io is a turn-based game engine library — game logic lives in src/, public entry points in packages/, and rollup-built bundles in dist/. It is published on npm as boardgame.io. The toolchain runs on a modern Node 24/26 baseline and uses pnpm as the package manager.
CONTRIBUTING.md governs how changes land, and applies to agent-authored work too. In short:
commit to a separate branch rather than main, keep each commit to one concern, make sure
pnpm test and pnpm run lint pass, and open a Pull Request. Read it before opening one.
pnpm 10.16+ is mandatory (pinned via packageManager in package.json). Don't use npm or yarn — pnpm-lock.yaml is the only lockfile, and CI installs with --frozen-lockfile. Enable via corepack enable.
.npmrc sets minimum-release-age=2880 (48 hours) as supply-chain protection. The integration script overrides this with --config.minimum-release-age=0 because it installs a local tarball.
pnpm settings live in pnpm-workspace.yaml, not package.json. overrides, dependency pins and other pnpm config were moved there in #1273. If pnpm config also exists in package.json, package.json wins and the workspace file is ignored entirely — so adding a pnpm.overrides block to package.json silently disables every existing pin (e.g. jest) with no warning.
pnpm test— runslint(pretest hook) then jest. Lint failures block tests.pnpm test:watch— jest in watch mode (no lint).pnpm exec jest path/to/file.test.ts— run one test file. Add-t "name"for one test case.pnpm run test:coverage— lint + jest with coverage scoped tosrc/**. This is whatpre-pushruns, so pushes aren't fast.pnpm run test:integration—scripts/integration.jsdeletesdist/,npm packs the lib, installs the tarball intointegration/, and runs that project's tests + build. Use to verify the published artifact.pnpm run lint/pnpm run lint:fix— eslint over the whole repo (.eslintrc).pnpm run ts— typecheck only (tsc --noEmit).pnpm run build— rollup build intodist/(silent).pnpm run build:watch— rollup in watch mode.pnpm start/pnpm dev— boots the react-web example (examples/react-web) with a dev server.devaddsbuild:watch.pnpm run docs— local docsify preview.
pre-commit runs lint-staged (prettier on staged .ts/.js/.css/.md). pre-push runs full coverage.
All real code lives under src/, split by domain:
core/— pure game engine: the reducer, flow (phases/turns/stages), turn-order strategies, initialization, action creators/types. Framework-agnostic, no I/O.client/— client-side runtime, framework bindings (react.tsx,react-native.js), and transports (transport/local.ts,transport/socketio.ts,transport/dummy.ts) that connect a client to either a local in-process master or a remote server.client/debug/— Svelte-based debug panel. Svelte sources are transformed byjest-svelte-transformer.cjsfor tests and viarollup-plugin-sveltefor builds.master/— server-side game authority. The Master applies moves, filters per-player views (filter-player-view.ts), and is what bothLocal(in-process) and the socket.io server transport drive.server/— Koa-based HTTP API + socket.io transport + DB adapters (db/flatfile.ts,db/inmemory.ts,db/localstorage.ts, plusdb/base.tsfor custom stores).ai/— bot framework (bot.ts,random-bot.ts,mcts-bot.ts) plusStep/Simulatehelpers.plugins/— plugin system + built-ins (plugin-immer,plugin-random,plugin-log,plugin-player,plugin-events,plugin-serializable). Plugins extend the per-move context (ctx.G,ctx.events, …).lobby/,testing/— matchmaking helpers and test utilities.types.ts— shared TS types (excluded from coverage).
Each packages/<name>.ts is a thin re-export file that defines a public subpackage. The canonical list is subpackages.js:
client, core, debug, react, react-native, ai, plugins, master, multiplayer, internal, testing
packages/server.ts is built separately (CJS-only, with full node deps). packages/main.js is the legacy single-bundle entry.
To add a new subpackage: create packages/<name>.ts, add <name> to subpackages.js, and add <name> to the files array in package.json.
Rollup produces four output groups from one config:
- Subpackages →
dist/esm/<name>.js+dist/cjs/<name>.js+dist/types/packages/<name>.d.ts(typescript declarations). - Server →
dist/cjs/server.js(CJS only, includes commonjs plugin for node deps). - Legacy combined →
dist/boardgameio.js(CJS) +dist/boardgameio.es.js(ESM) frompackages/main.js. - Browser UMD →
dist/boardgameio.min.js, minified, withprocess.env.NODE_ENVreplaced and filesize reporting.
prepack runs build then scripts/proxy-dirs.js, which generates top-level proxy directories (client/, core/, react/, …) each containing a tiny package.json pointing at dist/cjs|esm|types. This is what makes import 'boardgame.io/client' work for consumers. These dirs are gitignored but listed in the files field for publishing. postpack runs clean.
- Preset:
ts-jest/presets/js-with-babel, envjsdom. - Svelte: custom
jest-svelte-transformer.cjsplusmoduleNameMapperentries pointingsvelteandsvelte-json-tree-autoat their client builds. - Ignored paths:
examples/,integration/,node_modules/,.npm/. Don't put real tests in those dirs. - Setup files:
jest.setup.js,raf/polyfill,jest-date-mock, plus@testing-library/jest-domafter env.
babel.config.js aliases boardgame.io → ./packages. This lets src/ and examples/ import from the package name as if it were installed, without circular dependencies. Don't import from dist/ in source.
- Two pnpm projects nested:
examples/react-web/andintegration/each have their ownpnpm-lock.yaml. Don't conflate them with the root install. *.tgzis gitignored becausescripts/integration.jsrunsnpm packand leaves the tarball in the root. Don't commit it.- Coverage is collected only with the flag.
pnpm test(no flag) does not produce coverage; usepnpm run test:coverageif a hook or CI expectscoverage/lcov.info. - The
pretesthook runs lint, so a lint error makespnpm testfail before any test runs. Usepnpm exec jest …to skip lint when iterating on a single test.