Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: Set up the workspace
description: pnpm, Node, dependencies, and optionally a Playwright browser, the Naga validator and the built dist.

inputs:
node-version:
description: Node version for setup-node
default: '24.x'
browser:
description: Playwright browser to install (chromium or firefox); empty installs none
default: ''
Expand All @@ -26,9 +23,11 @@ runs:
with:
run_install: false

# The Node version comes from `.nvmrc`, the same file `devEngines` in
# package.json enforces locally, so CI and a contributor's shell agree.
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
node-version-file: .nvmrc
check-latest: true
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
# No install: the planner is dependency-free TypeScript that node strips.
- uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: .nvmrc

- id: plan
env:
Expand Down Expand Up @@ -303,7 +303,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: .nvmrc
- env:
NEEDS: ${{ toJSON(needs) }}
PLAN: ${{ toJSON(needs.plan.outputs) }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
# the developer's machine.
- uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: .nvmrc

- id: resolve
env:
Expand Down Expand Up @@ -144,7 +144,7 @@ jobs:
- uses: ./.github/actions/setup
- uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: .nvmrc
registry-url: 'https://registry.npmjs.org'

- uses: actions/download-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ docs/
!.gitattributes
!.gitignore
!.gitkeep
!.nvmrc
!.prettierignore
!.prettierrc

Expand Down
44 changes: 0 additions & 44 deletions .husky/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@
# push that brought the commit to main; a tag that would be
# rejected remotely is rejected here first.
#
# Also, on branch pushes only: a path-gated `@codexo/exojs-bench` typecheck
# (see the block below verify:quick — it is intentionally NOT part of
# verify:quick/CI; see packages/exojs-bench/README.md for why).
#
# Refs being pushed are read from stdin in the format:
# <local_ref> <local_sha> <remote_ref> <remote_sha>
is_tag_push=0
Expand Down Expand Up @@ -105,43 +101,3 @@ if [ "$is_branch_push" = "1" ]; then
npm run lanes -- --run --tests-only --all || exit 1
fi
fi

# Path-gated bench typecheck. @codexo/exojs-bench is deliberately kept out of
# verify:quick / CI: its competitor devDependencies (pixi/phaser/excalibur/
# matter/rapier) only resolve via `bench:setup`'s `pnpm install --dir
# competitors --ignore-workspace`, which bypasses pnpm-workspace.yaml's
# minimumReleaseAge supply-chain gate — installing that inside the shared-CI
# trust boundary is exactly what we don't want. So this hook is the only
# automated backstop, and it stays cheap for everyone else:
# - fires ONLY when the pushed commits touch packages/exojs-bench/** (zero
# cost otherwise — no diff, no pnpm call);
# - only runs the actual typecheck when the competitor deps are already
# linked locally (never forces the ~235MB bench:setup install on push —
# that would punish a push just because the optional deps aren't there
# yet, so it prints a warning and skips instead of failing).
# Residual gap (see packages/exojs-bench/README.md): an engine API change
# under src/ that breaks the bench adapters' types, without ALSO touching
# packages/exojs-bench/**, is not caught here.
if [ "$is_branch_push" = "1" ] && [ -n "$push_head_sha" ]; then
if [ -z "$push_base_sha" ]; then
# New branch / no remote tracking ref yet — fall back to the
# merge-base with origin/HEAD so the check still has a diff range.
push_base_sha=$(git merge-base "$push_head_sha" origin/HEAD 2>/dev/null || true)
fi

bench_changed=""
if [ -n "$push_base_sha" ]; then
bench_changed=$(git diff --name-only "$push_base_sha" "$push_head_sha" -- packages/exojs-bench 2>/dev/null)
fi

if [ -n "$bench_changed" ]; then
if [ -e packages/exojs-bench/node_modules/pixi.js ]; then
echo "[pre-push] packages/exojs-bench changed — running bench typecheck"
pnpm --filter @codexo/exojs-bench typecheck || exit 1
else
echo "[pre-push] WARNING: packages/exojs-bench changed but its competitor deps aren't linked locally."
echo "[pre-push] Run 'pnpm --filter @codexo/exojs-bench bench:setup' (or 'pnpm typecheck:bench') to type-check it, then push again."
echo "[pre-push] Skipping bench typecheck for this push (not blocking)."
fi
fi
fi
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ packages/exojs-bench/results/*.json

*.min.*
pnpm-lock.yaml

# API docs are generated here before being moved into src/content/api.
site/.api-staging
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ the file. `pnpm release:changelog` previews what the cut would add.
Run this once per clone:

```sh
pnpm bootstrap:dev
git config pull.ff only
```

`bootstrap:dev` installs the dependencies, the git hooks, every build output, the benchmark
competitor libraries and a Chromium for the browser lanes, then runs `pnpm doctor` - which
you can run again at any time to see what a clone is missing and how to fix it. The plain
`pnpm bootstrap` is the CI form: dependencies and build tooling only, with install scripts
disabled - which means it does not itself install the git hooks. `pnpm exec husky` adds them,
and `pnpm doctor` says whether they are there.

`main` and `next` only ever advance through a reviewed PR, so a local commit on
either cannot reach the remote — it just makes the branch diverge from its
remote counterpart. On a diverged branch, Git's default `pull` silently builds
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,27 @@ new Application({ backend: { type: 'auto' } }); // default

## Development

Prerequisites: Node 24 (`.nvmrc`; `devEngines` in `package.json` refuses any other major) and
pnpm (`packageManager` pins the version; with Corepack enabled, or any installed pnpm 10+, it
switches itself).

```bash
pnpm bootstrap:dev # dependencies, git hooks, every build, the bench competitors, a Chromium
pnpm doctor # what is missing, and the command that fixes it
```

`pnpm bootstrap` alone is what CI runs: dependencies and the build tooling, nothing else. It
installs with scripts disabled, so whether a clone ends up with git hooks depends on whether pnpm
ran an install of its own first - and it builds nothing. `pnpm doctor` reports the actual state
either way.

```bash
pnpm bootstrap
pnpm typecheck
pnpm lint
pnpm test
pnpm build
pnpm build:all # core plus every extension package
pnpm verify:package
pnpm clean:artifacts # what a local test, benchmark or release run left behind
```

Package-internal imports use Node `package.json#imports` subpath imports: `./X` for the same directory, `#dir/X` for any other path in the same package, and the public bare specifier (`@codexo/exojs`) across packages. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full import policy, per-package commands, and the shared `@codexo/exojs-config` tooling. Building the library requires TypeScript 6.
Expand Down
22 changes: 17 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
"version": "0.17.0",
"type": "module",
"packageManager": "pnpm@11.4.0",
"devEngines": {
"runtime": {
"name": "node",
"version": "^24",
"onFail": "error"
}
},
"files": [
"dist/esm/",
"dist/exo.esm.js",
Expand Down Expand Up @@ -73,10 +80,16 @@
"{src,test,examples,scripts,packages}/**/*.{ts,tsx,mts,cts}": "eslint --fix --no-warn-ignored --max-warnings=0"
},
"scripts": {
"bootstrap": "pnpm install --frozen-lockfile --ignore-scripts && pnpm build:tooling && pnpm bench:setup",
"bootstrap": "pnpm install --frozen-lockfile --ignore-scripts && pnpm build:tooling",
"bootstrap:dev": "pnpm bootstrap && pnpm exec husky && pnpm build:all && pnpm bench:setup && pnpm exec playwright install chromium && pnpm doctor",
"doctor": "tsx ./scripts/doctor.ts",
"build:tooling": "pnpm --filter @codexo/exojs-build build",
"clean": "rimraf dist",
"clean:artifacts": "tsx ./scripts/clean-artifacts.ts",
"clean:all": "tsx ./scripts/clean-artifacts.ts --all",
"build": "pnpm build:tooling && pnpm clean && tsx scripts/build.ts",
"build:packages": "pnpm -r --filter \"@codexo/exojs-*\" --filter \"!@codexo/exojs-examples\" --filter \"!@codexo/exojs-build\" --filter \"!@codexo/exojs-config\" --filter \"!@codexo/exojs-bench\" build",
"build:all": "pnpm build && pnpm build:packages",
"build:dev": "pnpm build:tooling && pnpm clean && tsx scripts/build.ts --dev",
"build:watch": "tsx scripts/build.ts --dev --watch",
"verify:exports": "tsx ./scripts/verify-exports.ts",
Expand Down Expand Up @@ -143,10 +156,11 @@
"lint:shaders": "tsx scripts/check-shader-sources.ts",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest run --project=exojs --project=exojs-build --project=exojs-particles --project=exojs-tilemap --project=exojs-tiled --project=exojs-physics --project=exojs-tilemap-physics --project=exojs-lighting --project=exojs-pathfinding --project=exojs-audio-fx --project=exojs-aseprite --project=exojs-ldtk --project=exojs-react --project=rendering-perf --project=rendering-alloc",
"test": "vitest run --project=exojs --project=exojs-build --project=exojs-particles --project=exojs-tilemap --project=exojs-tiled --project=exojs-physics --project=exojs-tilemap-physics --project=exojs-lighting --project=exojs-pathfinding --project=exojs-audio-fx --project=exojs-aseprite --project=exojs-ldtk --project=exojs-react --project=exojs-bench --project=rendering-perf --project=rendering-alloc",
"test:core": "vitest run --project=exojs",
"test:coverage": "vitest run --coverage --project=exojs --project=exojs-build --project=exojs-particles --project=exojs-tilemap --project=exojs-tiled --project=exojs-physics --project=exojs-tilemap-physics --project=exojs-lighting --project=exojs-pathfinding --project=exojs-audio-fx --project=exojs-aseprite --project=exojs-ldtk --project=exojs-react --project=rendering-perf",
"test:alloc": "vitest run --project=rendering-alloc",
"test:physics-perf": "vitest run --project=physics-perf",
"test:watch": "vitest --project=exojs",
"test:production-stripping": "vitest run --project=exojs test/build-defines/production-stripping.test.ts",
"test:skips": "pnpm run test --reporter=default --reporter=junit --outputFile.junit=./test-results/unit.junit.xml && tsx scripts/check-skipped-tests.ts",
Expand All @@ -168,9 +182,7 @@
"test:parity:webkit": "tsx ./scripts/run-parity.ts --project=browser-parity-webkit",
"test:parity:safari": "tsx ./scripts/run-parity.ts --project=browser-parity-safari",
"webgpu:probe": "tsx scripts/webgpu-probe.ts",
"bench": "vitest bench",
"bench:run": "vitest bench --run",
"site:install": "pnpm bootstrap",
"bench:micro": "vitest bench --run",
"site:build": "tsx scripts/check-dist-fresh.ts && pnpm --filter @codexo/exojs-examples build",
"site:build:api": "pnpm --filter @codexo/exojs-examples build:api",
"test:examples:smoke": "pnpm --filter @codexo/exojs-examples examples:smoke",
Expand Down
97 changes: 35 additions & 62 deletions packages/exojs-bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,65 +681,38 @@ caveats stated with the result, and a defined process for re-measuring when any
those move. Until that exists, a cross-library number measured here is an
engineering signal for the maintainers, not a claim.

## Why this package is out of required CI

`bench:setup` runs `pnpm install --dir competitors --ignore-workspace`. That
`--ignore-workspace` install:

- resolves a **separate lockfile** outside `pnpm-workspace.yaml`, so it
bypasses the root workspace's `minimumReleaseAge` supply-chain quarantine
(see `pnpm-workspace.yaml`) — a version bump here needs a manual release-age
sanity check instead of the automatic gate everything else gets;
- pulls in ~235MB of competitor libraries that a normal contributor should
never have to download just to typecheck their own PR.

Running that inside the shared-CI trust boundary (a required, always-on gate)
would mean every contributor's PR — and the shared CI runners — install and
trust third-party libraries whose only purpose is being compared against, not
shipped. So `@codexo/exojs-bench` is deliberately excluded from
`typecheck:packages` / `verify:quick` / CI. A standalone `typecheck:bench`
root script exists for on-demand/manual runs:

**The one exception is the structural gate**, and it is an exception precisely
because it needs none of that: it measures only the ExoJS arms on the software
rasterizer, so its CI job installs no competitor library and needs no GPU.
Nothing in that job runs `bench:setup`, so the competitor packages never enter
the CI trust boundary. It is path-gated on the rendering source, the harness and
the baseline itself — narrower than the `engine` area, since a change to audio or
input cannot move a draw-call count.

```sh
pnpm typecheck:bench # bench:setup + typecheck, in one step
```

## Local backstop: the pre-push hook

`.husky/pre-push` runs a **path-gated, local-only** check on branch pushes:

- it fires **only** when the commits being pushed touch
`packages/exojs-bench/**` — zero cost for every other push;
- if the competitor deps are already linked locally (i.e.
`packages/exojs-bench/node_modules/pixi.js` exists from a prior
`bench:setup`), it runs `pnpm --filter @codexo/exojs-bench typecheck` and
**fails the push** on a type error;
- if they aren't linked, it prints a warning telling you to run `bench:setup`
and **skips without failing** — an optional, uninstalled dependency should
never block an unrelated push.

## Known gap

This is a local, path-gated backstop, not a CI gate — it only runs on the
machine that pushes a bench-touching commit, and only if that machine has
already run `bench:setup`. An engine API change under `src/` that breaks the
bench adapters' types, without a commit that also touches
`packages/exojs-bench/**`, is not caught by this hook.

The structural-gate CI lane closes part of that gap, but only part: a change
under `src/rendering/` now runs the harness (and therefore compiles and executes
the ExoJS adapters) in CI, so a break there fails a PR. A change elsewhere under
`src/` still does not, and neither does anything that only affects a competitor
adapter. This is
an accepted trade-off to keep the bench package's ~235MB of competitor
dependencies out of the shared-CI trust boundary entirely. A future
self-hosted-GPU bench tier (see the engine's perf-tracking roadmap) is the
right place to run a full, unconditional `typecheck:bench` as a real backstop.
## What runs in CI, and what does not

Three different things live in this package, and CI treats them differently.

**The harness's own tests** (`test/`) exercise the profile contract, the slug,
the signature, the run pooling and the archetype definitions. They need none
of the competitor libraries - the adapters that import those are loaded by
`import()` inside the benchmark page, never by a test - so they run in the
ordinary `pnpm test` project list on every push, like any other package.

**The typecheck** does need the competitors: the adapters are typed against
their APIs, which is exactly what catches an upstream change on a version
bump. `pnpm typecheck:bench` installs them first. It runs in the path-gated
`bench` CI lane, alongside the structural gate, whenever a change touches this
package, the rendering source or the baselines.

**Measurements** never run in CI. A shared runner is neither idle nor a known
machine, and a number it produced would carry provenance nobody can reproduce.
Reference profiles are measured by hand on an idle machine and committed as
signed files - see `results/README.md`.

## The competitor install and the supply-chain gate

`bench:setup` runs `pnpm install --dir competitors --frozen-lockfile`. The
`competitors/` directory is its own workspace root (it carries a
`pnpm-workspace.yaml`), which does two things: a plain root `pnpm install`
never resolves or downloads anything in it, so a contributor who never
benchmarks pays nothing for ~235MB of libraries whose only purpose is being
compared against; and the install applies the same `minimumReleaseAge`
quarantine the repository workspace enforces, so a version bump here is held
back exactly as long as any other dependency. The lockfile is frozen in both
CI and local use: a new version enters through a reviewed lockfile change, not
through an install.

`pnpm doctor` reports whether the competitors are linked.
2 changes: 1 addition & 1 deletion packages/exojs-bench/competitors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "exojs-bench-competitors",
"version": "0.0.0",
"private": true,
"description": "Pinned exact-version competitor libraries for @codexo/exojs-bench (Pixi, Phaser, Excalibur, matter-js, planck, rapier2d-compat arms). Deliberately NOT a pnpm-workspace.yaml member: a plain root `pnpm install` never resolves or downloads anything here, so a normal contributor pays zero weight for competitor libraries. Only `pnpm --filter @codexo/exojs-bench bench:setup` installs this folder (via `pnpm install --ignore-workspace`, so a supply-chain review of a version bump should also sanity-check its release age by hand -- `--ignore-workspace` means the root pnpm-workspace.yaml's minimumReleaseAge gate does not apply here) and links the results into ../node_modules so the adapters' plain `import 'pixi.js'` (etc.) resolve unmodified.",
"description": "Pinned exact-version competitor libraries for @codexo/exojs-bench (Pixi, Phaser, Excalibur, matter-js, planck, rapier2d-compat arms). Deliberately NOT a member of the repository workspace: a plain root `pnpm install` never resolves or downloads anything here, so a normal contributor pays zero weight for competitor libraries. Only `pnpm --filter @codexo/exojs-bench bench:setup` installs this folder - as its own workspace root (see pnpm-workspace.yaml beside this file), which applies the same minimumReleaseAge supply-chain quarantine as the repository workspace - and links the results into ../node_modules so the adapters' plain `import 'pixi.js'` (etc.) resolve unmodified.",
"dependencies": {
"pixi.js": "8.19.0",
"phaser": "4.2.1",
Expand Down
6 changes: 6 additions & 0 deletions packages/exojs-bench/competitors/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Standalone install root for the benchmark competitor libraries. Keeping a
# workspace file here makes pnpm stop its upward lookup at this directory, so
# the install never joins the repository workspace - and it applies the same
# supply-chain quarantine the root workspace enforces, which an
# --ignore-workspace install would silently skip.
minimumReleaseAge: 4320
2 changes: 1 addition & 1 deletion packages/exojs-bench/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"type": "module",
"scripts": {
"bench:setup": "pnpm install --dir competitors --ignore-workspace && node competitors/link.ts",
"bench:setup": "pnpm install --dir competitors --frozen-lockfile && node competitors/link.ts",
"bench": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/run.ts",
"perf:baseline": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/run.ts",
"gate:timing": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/runTimingGate.ts",
Expand Down
Loading
Loading