ADR-0018 · accepted
The kit is one package with entries
Since ADR-0020: the package has twenty-one entries.
game,game/browserandgame/scenario(a game's declarations, its engine and its scenarios) are a tenth group, which may importcoreandruntime.
Since ADR-0019: the package has eighteen entries.
net/worker(the signalling worker, for Cloudflare) andnet/testing(test helpers for a Co-op game) joined the sixteen below. Both are in thenetgroup, so the graph has no new arrow.net/workermay importcloudflare:workers, with@cloudflare/workers-typesas an optional peer for its types;tsconfig.worker.jsonchecks it with Workers types and no DOM.
ADR-0016 published the kit as nine packages under @arcadekit, on one shared version. This replaces that layout with one package. ADR-0016's other decisions stand: the name, the licence, the build, the source condition, what users need, the checks, and releasing with Changesets and trusted publishing.
Why
Nine packages cost more than they gave.
- They never released apart. One version covered all nine, so every release was nine publishes, nine changelogs with the same lines and nine trusted publishers.
- A user installed several of them and had to keep their versions equal by hand. A game on the kit listed up to nine dependencies that only work together.
- Each package had its own
package.json,tsconfig, build config and peer ranges to keep in step. - What the split did enforce, that
coreandinputcarry no Effect and that a lower layer doesn't import a higher one, can be enforced by lint and a test.
Decision
One package. The kit publishes as the unscoped arcadekit, from packages/arcadekit. It starts at 0.3.0, the minor after the nine packages' 0.2.3, and its changelog keeps their entries.
Sixteen entries, carried over. Each entry of the nine packages is a subpath export of the same name, with the same exports: @arcadekit/core is arcadekit/core, and @arcadekit/net/browser is arcadekit/net/browser. The entries are core, input, content, content/browser, save, save/browser, audio, audio/browser, render-three, runtime, runtime/browser, runtime/testing, net, net/browser, net/signalling and foldkit. The package has no root export, so an import always names an entry. Each entry's source is packages/arcadekit/src/<entry>/, with its README beside it.
No shims. The nine @arcadekit/* packages stay on npm at 0.2.3 and get no further release. They are deprecated with a message naming arcadekit once the games that use them have moved.
Peer dependencies. effect is the one required peer: every entry but core and input imports it. three, @types/three, foldkit and @effect/platform-browser are optional peers, so a game installs only what the entries it imports need. The ranges are ADR-0016's.
Lint and a test replace the package boundaries.
oxlint.config.tslists which entry may import which: the graph the nine packages had.core,input,content,save,audioandrender-threeimport no other entry;runtimeimportscore;netimportscoreandruntime;foldkitimportsruntime, which only its tests use. The ruleentry-direction/allowed(tools/lint/entry-direction.ts) fails any other import between entries. It also fails an import that reaches into another entry's files instead of its public file, and one that names the package itself (arcadekit/corefrom inside the package), since a relative import says the same thing without going throughexports. Adding an arrow to the graph is a design decision, made in the PR that adds it.coreandinputstay free of Effect (ADR-0017), and each optional peer stays in the entry it is for. Lint banseffectin the files ofcoreandinput, andpackages/arcadekit/test/entry-packages.test.tslists the packages each entry may import: none forcoreandinput;effectfor the rest, withthreeforrender-three,foldkitforfoldkitand@effect/platform-browserforsave/browser. It follows every import from each entry, insrc/and in the builtdist/(the JavaScript and the type files), and fails if any reaches another package. The built check matters because the build shares chunks between entries. Inside the package a dynamic import names its module in a plain string, so lint and the test can read it.packages/arcadekit/tsconfig.root.jsoncompilescoreand the root entries ofcontent,save,audio,runtimeandnet, withnet/signalling, without DOM types, as each package's own config did.
One build. tsdown builds every entry in one pass, sharing chunks, so a module two entries use exists once: arcadekit/runtime and arcadekit/net step the same GameLoop.
When to split again
Either of these would justify a second package:
- someone outside this repository needs a subset, for example
corewithout theeffectpeer; - an entry needs a dependency that can't be an optional peer, such as one with a native build or one that every install would have to download.
Until then, a new part of the kit is a new entry.
Consequences
- Every import of the kit changes, from
@arcadekit/<entry>toarcadekit/<entry>. The genre packages and the demo apps changed with it; a game outside the workspace changes its imports and replaces its@arcadekit/*dependencies witharcadekit. - The keys of the Effect services follow the names:
@arcadekit/audio/Audioisarcadekit/audio/Audio, and@arcadekit/net/PeerConnectorisarcadekit/net/PeerConnector. Only a program that had both the old and the new package would notice. - A package manager warns about nothing when an optional peer is missing. A game that imports
arcadekit/render-threewithoutthreefails when it resolves the import, so each entry's README names its peers. - The genre packages keep their names (
@arcadekit/survivors,@arcadekit/push-puzzle) and stay private. - Earlier ADRs and findings keep their wording: where they say
@arcadekit/runtimeorpackages/net/src, readarcadekit/runtimeandpackages/arcadekit/src/net. - Changesets has one package to version, so its
fixedgroup is gone. - npm needs the package to exist before it can trust the release workflow. The maintainer publishes a
0.0.0placeholder once (scripts/npm-setup.sh), and the workflow publishes every real version.