chess-study-stockfish
v0.1.0
Published
Vendoring script for chesStudy's Stockfish 18 WASM builds, and the corresponding source for the shared-memory-ceiling patch applied to them
Maintainers
Readme
chess-study-stockfish
The vendoring script for chesStudy's Stockfish 18 WASM builds — and, more importantly, the corresponding source for the modification chesStudy applies to those builds before serving them.
pnpm add -D chess-study-stockfish stockfish
pnpm vendor-stockfish --out public/stockfishWhy this repository exists
chesStudy serves Stockfish to browsers as static .js + .wasm files. It does not serve
upstream's bytes: the threaded build is byte-patched first (see below). Distributing a
modified GPL-3.0 program obliges whoever distributes it to offer the Corresponding
Source for what was distributed, to the people who received it — GPL-3.0 §6. Everyone
who loads the app is such a recipient.
This repository is that offer's address. Two things follow from that, and both are deliberate:
- It contains our modification, not Stockfish. §6's final paragraph allows the unmodified remainder to be covered by clear directions to where it can be obtained, so the honest content here is the patch plus the upstream coordinates — not a fork of an engine we did not write.
- Being consumed by the app is not the point, but it is the mechanism. The obligation is to publish an address, not to restructure anyone's build. But an offer only discharges anything while the published source still corresponds to the shipped bytes, and two hand-synced copies of a patch script drift silently. So chesStudy depends on this package from npm and runs this code — the published copy is the one that runs, by construction, and cannot fall out of date.
What the patch does
Upstream's threaded build (stockfish-18-lite.js + .wasm) declares an imported shared
memory with a maximum of 32768 WASM pages — 2 GiB. Shared memory is reserved eagerly, so
that allocation simply fails on memory-constrained devices, and the engine never starts.
chesStudy only uses threaded mode for depth- and time-limited live evaluation, so it does
not need 2 GiB.
Vendoring therefore rewrites the ceiling to 8192 pages (512 MiB), in two places that have to agree:
| File | What changes |
| --- | --- |
| stockfish-18-lite.wasm | the maximum field of the imported memory in the import section, rewritten in place as padded LEB128 so the field's byte width is unchanged and no later section offset shifts |
| stockfish-18-lite.js | the glue's new WebAssembly.Memory({initial:…,maximum:32768,shared:!0}) literal |
Both, because a JS-supplied WebAssembly.Memory whose maximum is smaller than the
module's declared import maximum fails import validation. That is also why the ceiling
cannot simply be lowered at runtime instead — the declared import is the thing that has to
change, which is what makes this a binary modification rather than a configuration choice.
stockfish-18-lite-single.js + .wasm — the single-threaded fallback — are copied
byte-identical to upstream. Nothing is patched there.
The consumer's runtime pre-flight probe must test the same size it vendored. In chesStudy
that is THREADED_MAX_WASM_PAGES in src/lib/engine/EngineClient.ts.
Upstream
| | |
| --- | --- |
| Package | stockfish on npm (nmrugg/stockfish.js), a peer dependency here |
| Version chesStudy ships | see the stockfish entry in chesStudy's lockfile; the vendor run prints the version it patched |
| Upstream project | official-stockfish/Stockfish |
| License | GPL-3.0-or-later |
Reproducing the shipped binaries
git clone https://github.com/yoavniran/chess-study-stockfish
cd chess-study-stockfish
pnpm install # pulls the upstream `stockfish` package
node bin/vendor-stockfish.mjs --out ./out./out now holds the four files chesStudy serves. Pin the same stockfish version the app
ships and the output is byte-for-byte what is deployed. pnpm test additionally patches the
real upstream wasm and compiles it, which is the claim above stated as an assertion.
Usage
CLI
vendor-stockfish [options]
--out <dir> where to write the builds (default: public/stockfish)
--from <dir> source directory (default: the `stockfish` package's bin/)
--max-pages <n> threaded shared-memory maximum, in 64 KiB WASM pages
(default: $SF_MAX_PAGES, else 8192 = 512 MiB)
--if-env <NAME> do nothing when $NAME is 0, false or off
--quiet no output on success
-h, --help this message--if-env exists for builds that switch the engine off: chesStudy runs
vendor-stockfish --if-env VITE_FEAT_ENGINE, and an engine-off build skips ~14 MB of WASM
it would never serve. An unset or unrecognised value counts as on, so a default build
always vendors.
The upstream bin/ directory is resolved from the current working directory, so it is
the consumer's copy at the consumer's pinned version.
From a script
import { vendorStockfish } from 'chess-study-stockfish';
const { patch, upstreamVersion } = vendorStockfish({
outDir: 'public/stockfish',
log: console.log,
});
// patch -> { from: 32768, to: 8192 }chess-study-stockfish/wasm-memory exposes the byte-level primitives on their own
(patchImportedMemoryMax, encodeLebPadded, WASM_PAGE_BYTES) for anyone who needs the
same surgery on a different module.
Development
The package ships plain ESM JavaScript — no bundle, no transpile. What npm installs is the
source, which is the point of a corresponding-source repository. TypeScript is here only to
type-check the JSDoc and emit .d.mts declarations.
pnpm install
pnpm typecheck # tsc over src/, bin/, test/ with checkJs
pnpm test # vitest; the real-upstream test skips if `stockfish` is absent
pnpm build # emit dist/*.d.mts
pnpm changeset # describe a releaseCommits follow Conventional Commits, enforced by .husky/commit-msg locally and the
commits job in CI. Releases are changesets-driven: a PR that changes src/ or bin/
should add a changeset, and merging the "Version Packages" PR publishes to npm.
Publishing setup
Everything below is account and repository configuration — none of it lives in the
repo, and release.yml cannot succeed until it is done. It is a one-time list.
1. Let Actions open pull requests. Repo Settings → Actions → General → Workflow permissions → tick "Allow GitHub Actions to create and approve pull requests".
This defaults to off, and it is the first thing that breaks. release.yml already
requests pull-requests: write, but that permission is not sufficient — the repo toggle
overrides it. Without it the workflow pushes changeset-release/main and then fails with
GitHub Actions is not permitted to create or approve pull requests.
2. Publish 0.1.0 by hand, once. Trusted publishing is configured on a package's settings page, and a package that has never been published has no settings page:
npm login
pnpm build && npm publish3. Point the package at this workflow. On npmjs.com → the package → Settings → Trusted
publishers → GitHub Actions, with repository yoavniran/chess-study-stockfish and workflow
release.yml.
After this every later release is automatic, and no NPM_TOKEN secret is needed — OIDC
replaces it. Do not add one. Trusted publishing needs npm ≥ 11.5.1 and Node ≥ 22.14;
release.yml installs npm@latest rather than trusting the runner image.
License
GPL-3.0-or-later — see LICENSE. This code is corresponding source for a GPL-3.0-or-later work, so it carries the same license.
Using this package as a build-time tool does not make the software it builds GPL: it copies and patches files, it is not linked into anyone's application. What is GPL is what it produces — the Stockfish builds — which stay GPL-3.0-or-later wherever they are served, and must be accompanied by this notice and a link back here.
