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
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,90 @@ merged pull requests and commits since the previous tag (each with its commit /
PR link); `pnpm release:notes` then renders that section into the published
GitHub release with a `PREVIOUS_TAG...CURRENT_TAG` compare link.

## [0.14.0] - 2026-06-23

The architecture release. Two new packages — `@codexo/exojs-physics` (a native
2D collision, query, and dynamics world) and `@codexo/exojs-audio-fx` (the audio
effect suite, extracted from core) — join the lockstep set. Core gains a UI
layer, scene-graph serialization with prefabs and save files, immediate-mode
rendering, an ordered System scheduler, and a multi-instance-safe foundation
(deterministic disposal, app-owned managers). The Scene model is simplified to a
single active scene. This is a pre-1.0 release and includes intentional breaking
changes; see **Changed** and **Removed**.

### Added

- **`@codexo/exojs-physics` — native 2D physics.** Shapes, colliders, and bodies
with an SAP broadphase, manifold narrow-phase, and a warm-started
sequential-impulse solver (2×2 block normal LCP + NGS position correction,
pre-gravity restitution). Contact graph and events, spatial queries,
scene-graph binding, and a `/debug` draw subpath. Allocation-free per step;
the dynamics surface (`velocity`, `applyForce`/`Torque`/`Impulse`) is public
(#131, #140, #141, #142, #143, #155, #156).
- **`@codexo/exojs-audio-fx` — audio effect package.** Extracted from core: the
`*Effect` suite, `AudioAnalyser`, `BeatDetector`, worklets, and DSP helpers.
Core keeps the audio engine and effect base classes (#133).
- **UI core.** `scene.ui` with a `Widget`/`Label`/`Panel`/`Button`/`ProgressBar`
set, row/column/stack layout and anchoring, a `FocusManager` with keyboard
navigation, and `app.focus` (#138).
- **Scene-graph serialization.** `SerializationRegistry`, `NodeSerializer`,
`Prefab`, and `SaveManager` with `Scene.serialize`/`deserialize`; serializers
for containers, sprites, text, meshes, graphics, nine-slice/repeating sprites,
animated sprites, bitmap text, video, and UI widgets. Tilemap nodes serialize
through an extension binding (#144, #145, #146, #147, #148).
- **Immediate-mode rendering.** `RenderingContext.drawGeometry` for one-off
geometry and `RenderBatch` + `drawBatch` for instanced draws collapsing to a
single draw call (#150, #151, #159).
- **System scheduler.** `app.systems` and `scene.systems` run the core managers
as ordered systems; `update` signatures converge on `(delta: Time)` (#134).
- **Design-space coordinates.** Automatic DPR handling, letterbox sizing, and
`canvas`-mount / `sizingMode` options on `Application` (#130).
- **Typed tilemap object layers.** Object layers and queries converted from
Tiled object groups, plus an `ObjectKind` `as const` schema and a generic
`ObjectLayer<S>` with `byType`/`byKind`/`where` (#132, #157).
- **Combined Tiled + physics examples** with an `ObjectLayer`→collider bridge
(#160), a rebuilt example catalog on a shared runtime helper kit, and a live
hero example with an expandable playground preview.

### Changed

- **Audio re-architecture.** `Sound`/`AudioStream`/`AudioGenerator` descriptors
with a voice capability matrix; the audio singleton is gone and `AudioFilter`
becomes `AudioEffect`. Playback defers until the autoplay gesture unlocks
audio (#133).
- **Multi-instance foundation.** `Destroyable`/`DisposalScope` for deterministic
teardown; `Interaction`, `Audio`, `Random`, and the serializer registry are
app-owned rather than process singletons; `ObservableVector` sheds per-node
closures (#133, #134, #154).
- **BREAKING — API hygiene.** Value-type footgun fixes (`Matrix.getInverse`,
`Color.toRgba`, honest `Rectangle` types), curated barrels, and namespaced
utilities (`MathUtils`, `MeshBuilder`, `Sweep`, `Collision`, …) (#135).
- **BREAKING — `Random` engine.** Mersenne Twister replaced with xoshiro128\*\*
and SplitMix32 seeding; the `iteration` getter is removed (#137).
- **BREAKING — physics body construction.** `new PhysicsBody({ colliders })` +
`world.add`/`world.attach` replace `createBody`/`createStaticCollider` (#156).
- **BREAKING — rendering barrel.** Backend renderer internals move behind the
`@codexo/exojs/renderer-sdk` subpath; the root barrel is curated (#153).
- **Site islands migrated from Lit to React** (#149); a shared `Registry<K,V>`
primitive backs constructor-keyed dispatch (#136).

### Removed

- **BREAKING — scene stack.** `SceneStackMode`, `SceneParticipation`,
`pushScene`, and `popScene` are removed in favor of one active scene with
`setScene`, fade transitions, and `scene.paused` (#139).

### Fixed

- Physics contact pair keys no longer collide past 65,536 body IDs (#155).
- New and mutated textures upload correctly after their first bind, and pointer
coordinates map to backing-store pixels when the canvas is scaled (#130).

### Docs

- ADR on shared geometry with separate collision detection (#158) and an
immediate-mode rendering guide (#159).

## [0.13.0] - 2026-06-13

The scalable-sprites and tilemap release. `TextureRegion`, `NineSliceSprite`,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@codexo/exojs",
"description": "A TypeScript-first browser 2D runtime for games and interactive apps.",
"version": "0.13.0",
"version": "0.14.0",
"type": "module",
"packageManager": "pnpm@11.4.0",
"files": [
Expand Down
4 changes: 2 additions & 2 deletions packages/exojs-audio-fx/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexo/exojs-audio-fx",
"version": "0.13.0",
"version": "0.14.0",
"description": "Audio effects, DSP, beat detection and analysis for ExoJS.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -33,7 +33,7 @@
"test": "vitest run --root ../.. --project=exojs-audio-fx"
},
"peerDependencies": {
"@codexo/exojs": "0.13.x"
"@codexo/exojs": "0.14.x"
},
"devDependencies": {
"@codexo/exojs": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/exojs-particles/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexo/exojs-particles",
"version": "0.13.0",
"version": "0.14.0",
"description": "Particle system extension for ExoJS.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -47,7 +47,7 @@
"test": "vitest run --root ../.. --project=exojs-particles"
},
"peerDependencies": {
"@codexo/exojs": "0.13.x"
"@codexo/exojs": "0.14.x"
},
"devDependencies": {
"@codexo/exojs": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/exojs-physics/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexo/exojs-physics",
"version": "0.13.0",
"version": "0.14.0",
"description": "Native 2D collision, query and sensor runtime for ExoJS.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -38,7 +38,7 @@
"test": "vitest run --root ../.. --project=exojs-physics"
},
"peerDependencies": {
"@codexo/exojs": "0.13.x"
"@codexo/exojs": "0.14.x"
},
"devDependencies": {
"@codexo/exojs": "workspace:*",
Expand Down
4 changes: 2 additions & 2 deletions packages/exojs-tiled/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexo/exojs-tiled",
"version": "0.13.0",
"version": "0.14.0",
"description": "Tiled map format asset extension for ExoJS.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -40,7 +40,7 @@
"test": "vitest run --root ../.. --project=exojs-tiled"
},
"peerDependencies": {
"@codexo/exojs": "0.13.x"
"@codexo/exojs": "0.14.x"
},
"dependencies": {
"@codexo/exojs-tilemap": "workspace:*"
Expand Down
4 changes: 2 additions & 2 deletions packages/exojs-tilemap/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexo/exojs-tilemap",
"version": "0.13.0",
"version": "0.14.0",
"description": "Generic, format-independent tilemap runtime for ExoJS.",
"repository": {
"type": "git",
Expand Down Expand Up @@ -40,7 +40,7 @@
"test": "vitest run --root ../.. --project=exojs-tilemap"
},
"peerDependencies": {
"@codexo/exojs": "0.13.x"
"@codexo/exojs": "0.14.x"
},
"devDependencies": {
"@codexo/exojs": "workspace:*",
Expand Down
18 changes: 11 additions & 7 deletions scripts/release/RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
# Releasing ExoJS

The coordinated release publishes the four lockstep packages — `@codexo/exojs`,
`@codexo/exojs-particles`, `@codexo/exojs-tilemap`, `@codexo/exojs-tiled` — at one
shared version via the two-stage, build-once pipeline (`scripts/release/`).
The coordinated release publishes the six lockstep packages — `@codexo/exojs`,
`@codexo/exojs-particles`, `@codexo/exojs-tilemap`, `@codexo/exojs-tiled`,
`@codexo/exojs-physics`, `@codexo/exojs-audio-fx` — at one shared version via the
two-stage, build-once pipeline (`scripts/release/`).

## Normal release

1. Land everything on `main`. Curate the `## [Unreleased]` CHANGELOG section into
`## [x.y.z] - YYYY-MM-DD` (a concrete date — `release:notes` rejects "Unreleased").
2. Bump all four package versions in lockstep (and the peer ranges to `x.y.x`).
2. Bump all six package versions in lockstep (and the peer ranges to `x.y.x`).
3. Tag and push: `git tag -a vX.Y.Z <commit> -m "ExoJS vX.Y.Z" && git push origin refs/tags/vX.Y.Z`.
4. The `Release` workflow checks out the **tag**, runs the full CI gate, builds
once, packs/hashes/attw/consumer-tests the four tarballs, publishes them via
OIDC (Core → Particles → Tilemap → Tiled) to a staging dist-tag, promotes all
four to `latest`, and creates the GitHub release with the Full ZIP.
once, packs/hashes/attw/consumer-tests the six tarballs, publishes them via
OIDC (Core → Particles → Tilemap → Tiled → Physics → Audio-FX) to a staging
dist-tag, promotes all six to `latest`, and creates the GitHub release with
the Full ZIP.

The workflow checks out the **tag commit**, so any fix to the release _scripts_
must be on the tag — re-point the tag (`git tag -d` + `git tag -a` + force-push)
Expand Down Expand Up @@ -55,6 +57,8 @@ npm dist-tag add @codexo/exojs@X.Y.Z latest
npm dist-tag add @codexo/exojs-particles@X.Y.Z latest
npm dist-tag add @codexo/exojs-tilemap@X.Y.Z latest
npm dist-tag add @codexo/exojs-tiled@X.Y.Z latest
npm dist-tag add @codexo/exojs-physics@X.Y.Z latest
npm dist-tag add @codexo/exojs-audio-fx@X.Y.Z latest
```

The packages are already published and immutable at this point — this only moves
Expand Down
11 changes: 6 additions & 5 deletions scripts/verify-lockstep-versions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Verifies that all five official ExoJS packages share the same version.
* Verifies that all six official ExoJS packages share the same version.
*
* The lockstep version contract: @codexo/exojs, @codexo/exojs-particles,
* @codexo/exojs-tilemap, @codexo/exojs-tiled, and @codexo/exojs-physics must
* all be on the same X.Y.Z version for every coordinated release.
* @codexo/exojs-tilemap, @codexo/exojs-tiled, @codexo/exojs-physics, and
* @codexo/exojs-audio-fx must all be on the same X.Y.Z version per release.
*/
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
Expand Down Expand Up @@ -32,6 +32,7 @@ const extensionPkgs = [
readPackage('packages/exojs-tilemap/package.json'),
readPackage('packages/exojs-tiled/package.json'),
readPackage('packages/exojs-physics/package.json'),
readPackage('packages/exojs-audio-fx/package.json'),
];
const packages = [corePkg, ...extensionPkgs];

Expand All @@ -42,7 +43,7 @@ if (versions.length !== 1) {
for (const p of packages) {
process.stderr.write(` ${p.name}: ${p.version}\n`);
}
process.stderr.write('\nAll five packages must be on the same version before release.\n' + 'Update all five package.json files to the same version.\n');
process.stderr.write('\nAll six packages must be on the same version before release.\n' + 'Update all six package.json files to the same version.\n');
process.exit(1);
}

Expand All @@ -67,4 +68,4 @@ if (peerProblems.length > 0) {
process.exit(1);
}

process.stdout.write(`verify-lockstep: all 5 packages at v${versions[0]}; extension peer ranges = "${expectedPeer}" ✓\n`);
process.stdout.write(`verify-lockstep: all ${packages.length} packages at v${versions[0]}; extension peer ranges = "${expectedPeer}" ✓\n`);
35 changes: 3 additions & 32 deletions test/release/changelog-v0.13.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ const extractV013Section = (): string => {
return content.slice(afterStart + 1, nextSection === -1 ? undefined : nextSection);
};

/** Load package.json manifests for the 4 lockstep packages. */
const loadPackageJson = (name: string): Record<string, unknown> =>
JSON.parse(readFileSync(resolve(repoRoot, 'packages', name.replace('@codexo/', ''), 'package.json'), 'utf8'));

// Historical invariants for the shipped 0.13.0 section. Live package-version
// coherence is asserted by the CURRENT release's test (changelog-v0.14.test.ts),
// not here — that check moves forward with each release.
describe('v0.13 release text invariants', () => {
const section = extractV013Section();

Expand Down Expand Up @@ -83,31 +82,3 @@ describe('v0.13 release text invariants', () => {
expect(section).toMatch(/structured/);
});
});

describe('package manifest / changelog version coherence', () => {
const packages = ['@codexo/exojs-particles', '@codexo/exojs-tilemap', '@codexo/exojs-tiled'];

for (const pkg of packages) {
it(`${pkg} manifest version is 0.13.0`, () => {
const manifest = loadPackageJson(pkg);
expect(manifest.version).toBe('0.13.0');
});

it(`${pkg} peer dependency range is 0.13.x`, () => {
const manifest = loadPackageJson(pkg);
const peers = (manifest.peerDependencies ?? {}) as Record<string, string>;
expect(peers['@codexo/exojs']).toBe('0.13.x');
});
}

it('@codexo/exojs-tiled has @codexo/exojs-tilemap as a regular dependency', () => {
const manifest = loadPackageJson('@codexo/exojs-tiled');
const deps = (manifest.dependencies ?? {}) as Record<string, string>;
expect(deps['@codexo/exojs-tilemap']).toBeDefined();
});

it('root package version is 0.13.0', () => {
const root = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8'));
expect(root.version).toBe('0.13.0');
});
});
87 changes: 87 additions & 0 deletions test/release/changelog-v0.14.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { describe, expect, it } from 'vitest';

const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
const repoRoot = resolve(__dirname, '..', '..');
const changelogPath = resolve(repoRoot, 'CHANGELOG.md');

const readChangelog = (): string => readFileSync(changelogPath, 'utf8');

/** Extract the 0.14.0 section text (from its heading to the next `## [` heading). */
const extractV014Section = (): string => {
const content = readChangelog();
const startMarker = '## [0.14.0]';
const startIndex = content.indexOf(startMarker);
if (startIndex === -1) throw new Error('0.14.0 section not found in CHANGELOG.md');
const afterStart = content.indexOf('\n', startIndex);
const nextSection = content.indexOf('\n## [', afterStart + 1);
return content.slice(afterStart + 1, nextSection === -1 ? undefined : nextSection);
};

/** Load a lockstep extension package.json by its @codexo/ name. */
const loadPackageJson = (name: string): Record<string, unknown> =>
JSON.parse(readFileSync(resolve(repoRoot, 'packages', name.replace('@codexo/', ''), 'package.json'), 'utf8'));

// Matchers use single tokens (not multi-word phrases) because Prettier
// prose-wraps the changelog, so an inter-word match could span a newline.
describe('v0.14 release text invariants', () => {
const section = extractV014Section();

it('carries a concrete release date in the heading', () => {
// `release:notes` (publish job, AFTER npm publish) hard-fails on any
// heading that is not `## [0.14.0] - YYYY-MM-DD` — "Unreleased" would
// publish npm packages and then abort the GitHub release.
expect(readChangelog()).toMatch(/^## \[0\.14\.0\] - \d{4}-\d{2}-\d{2}$/m);
});

it('contains no #this placeholder', () => {
expect(section).not.toMatch(/#this/);
});

it('introduces the two new lockstep packages', () => {
expect(section).toMatch(/@codexo\/exojs-physics/);
expect(section).toMatch(/@codexo\/exojs-audio-fx/);
});

it('documents the breaking scene-stack removal', () => {
expect(section).toMatch(/SceneStackMode/);
});

it('documents the major additive features', () => {
expect(section).toMatch(/\bUI\b/);
expect(section).toMatch(/serializ/i);
expect(section).toMatch(/immediate-mode/i);
});
});

describe('package manifest / changelog version coherence', () => {
const packages = ['@codexo/exojs-particles', '@codexo/exojs-tilemap', '@codexo/exojs-tiled', '@codexo/exojs-physics', '@codexo/exojs-audio-fx'];

for (const pkg of packages) {
it(`${pkg} manifest version is 0.14.0`, () => {
const manifest = loadPackageJson(pkg);
expect(manifest.version).toBe('0.14.0');
});

it(`${pkg} peer dependency range is 0.14.x`, () => {
const manifest = loadPackageJson(pkg);
const peers = (manifest.peerDependencies ?? {}) as Record<string, string>;
expect(peers['@codexo/exojs']).toBe('0.14.x');
});
}

it('@codexo/exojs-tiled has @codexo/exojs-tilemap as a regular dependency', () => {
const manifest = loadPackageJson('@codexo/exojs-tiled');
const deps = (manifest.dependencies ?? {}) as Record<string, string>;
expect(deps['@codexo/exojs-tilemap']).toBeDefined();
});

it('root package version is 0.14.0', () => {
const root = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8'));
expect(root.version).toBe('0.14.0');
});
});
Loading