Skip to content

feat: @melonjs/capacitor-plugin + create-melonjs --template capacitor#1439

Open
obiot wants to merge 7 commits intomasterfrom
feat/capacitor-plugin
Open

feat: @melonjs/capacitor-plugin + create-melonjs --template capacitor#1439
obiot wants to merge 7 commits intomasterfrom
feat/capacitor-plugin

Conversation

@obiot
Copy link
Copy Markdown
Member

@obiot obiot commented May 10, 2026

Summary

Adds first-class Capacitor support to the melonJS monorepo:

  1. New package: @melonjs/capacitor-plugin — a BasePlugin-style adapter that bridges Capacitor's native lifecycle and hardware-back events into the engine. No engine changes were needed; this is purely an ergonomics layer over the standard @capacitor/app events.
  2. create-melonjs --template capacitor — new template flag that scaffolds from a Capacitor-flavored boilerplate (melonjs/typescript-boilerplate-capacitor, to be created separately).
  3. Wiki draft — full Capacitor guide saved to /tmp/Capacitor.md locally; needs to be pasted into the melonjs/melonJS.wiki repo.

What the plugin gives you

plugin.register(CapacitorPlugin, "capacitor", { pauseAudio: true });

class PlayStage extends Stage {
    onResetEvent() {
        bindStageBack(this, (evt) => {
            state.change(state.MENU);
            evt.preventDefault();   // suppress default App.exitApp()
        });
    }
}

vs. ~25 lines of raw App.addListener(...) boilerplate per game today.

Feature API What it does
Lifecycle pauseOnBackground, pauseAudio options appStateChangestate.pause/resume
Hardware back bindStageBack(stage, fn) / onBackButton(fn) backButton → engine event bus, evt.preventDefault() to suppress default exit
Orientation lockOrientation("portrait" | "landscape"), unlockOrientation() Lazy wrappers around @capacitor/screen-orientation
Splash hideSplash({ fadeOutDuration }) Lazy wrapper around @capacitor/splash-screen

@capacitor/app is the only required peer dep; orientation and splash deps are optional and dynamically imported only when the corresponding helpers are called.

Files

  • packages/capacitor-plugin/ — new package, ~150 LOC of source across 8 src/*.ts files plus the standard scaffolding (package.json, tsconfigs, scripts, README, CHANGELOG, LICENSE).
  • packages/create-melonjs/--template <name> flag, version bump 1.0.1 → 1.1.0, README + CHANGELOG.

Test plan

  • pnpm lint — 0 errors.
  • pnpm build (packages/capacitor-plugin) — emits build/index.js + .d.ts cleanly.
  • pnpm vitest run — 2933 melonjs tests still pass; no regressions.
  • node packages/create-melonjs/bin/create-melonjs.js — usage banner shows the new flag and templates.
  • node ... my-game --template bogus — errors out with the list of known templates.
  • End-to-end iOS/Android scaffold + run (deferred — needs melonjs/typescript-boilerplate-capacitor repo to exist).

Follow-ups (not in this PR)

  • Create melonjs/typescript-boilerplate-capacitor GitHub repo (Vite + melonJS + @capacitor/* + @melonjs/capacitor-plugin pre-wired). The --template capacitor flag will 404 until this repo exists.
  • Paste /tmp/Capacitor.md into the melonjs/melonJS.wiki repo.
  • Add a Capacitor logo to the README's "Tools integration" section.

🤖 Generated with Claude Code

obiot and others added 2 commits May 10, 2026 09:13
A `BasePlugin`-style adapter that bridges Capacitor's native
lifecycle and hardware-back events into the engine, so a melonJS
game wrapped as a hybrid iOS/Android app pauses on background,
resumes on foreground, and lets each Stage intercept hardware-back
without manual `event.on` / `event.off` boilerplate.

Public surface:
- `CapacitorPlugin` — register via
  `plugin.register(CapacitorPlugin, "capacitor", options?)`.
  Auto-wires `App.addListener("appStateChange")` →
  `state.pause/resume` and `App.addListener("backButton")` → engine
  event bus.
- `bindStageBack(stage, handler)` — per-stage back-button handler
  whose lifetime matches `onResetEvent` / `onDestroyEvent`. Handler
  receives a `CapacitorBackEvent` with `preventDefault()` to
  suppress the default action (typically `App.exitApp()`).
- `onBackButton(handler)` — global, stage-agnostic equivalent.
- `lockOrientation` / `unlockOrientation` / `hideSplash` — thin
  lazy wrappers around the matching Capacitor plugins
  (`@capacitor/screen-orientation` and `@capacitor/splash-screen`,
  declared as optional peer deps so they're only pulled when used).

Required runtime peer is `@capacitor/app`; melonJS peer pinned to
`>=19.3.0` (the version that shipped the surface this plugin
relies on).

Functionally a melonJS game already runs inside Capacitor's WebView
unmodified — this package is purely an ergonomics layer over the
standard `@capacitor/app` events. No engine changes were needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`npm create melonjs <name>` now accepts `--template <name>` (alias
`-t`). Two templates are recognized:

- `default` — current behavior, points at
  `melonjs/typescript-boilerplate`.
- `capacitor` — points at
  `melonjs/typescript-boilerplate-capacitor` (a separate boilerplate
  repo, to be created), pre-wired with
  `@melonjs/capacitor-plugin` for iOS/Android wrapping via Capacitor.

Unknown template names error out with the list of known templates.
The output now reports which template was used and prints
template-specific next-step instructions (the `capacitor` path
includes `cap add ios/android`, `cap copy`, `cap open`).

Bumped to 1.1.0. README updated with a templates table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 01:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Capacitor support to the melonJS monorepo by introducing a new Capacitor integration plugin package and extending create-melonjs to scaffold a Capacitor-oriented starter.

Changes:

  • Introduce new @melonjs/capacitor-plugin package to bridge Capacitor lifecycle + Android hardware-back into melonJS (plus optional orientation/splash helpers).
  • Add --template/-t support to create-melonjs, including a new capacitor template option and updated docs/changelog.
  • Update lockfile to include Capacitor-related dependencies for the new package.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds lock entries/importer config for the new packages/capacitor-plugin dependencies.
packages/create-melonjs/README.md Documents the new --template flag and available templates.
packages/create-melonjs/package.json Bumps version to 1.1.0 for the new feature.
packages/create-melonjs/CHANGELOG.md Records the new --template feature and behavior changes.
packages/create-melonjs/bin/create-melonjs.js Implements --template/-t parsing, template selection, and template-specific next steps.
packages/capacitor-plugin/tsconfig.json Adds TS config for local typechecking in the new package.
packages/capacitor-plugin/tsconfig.build.json Adds TS config for declaration-only builds into build/.
packages/capacitor-plugin/src/types.ts Defines public option and event types (ConnectCapacitorOptions, CapacitorBackEvent).
packages/capacitor-plugin/src/splash.ts Adds lazy @capacitor/splash-screen wrapper helper.
packages/capacitor-plugin/src/plugin.ts Adds CapacitorPlugin (BasePlugin) that installs the wiring on registration.
packages/capacitor-plugin/src/orientation.ts Adds lazy @capacitor/screen-orientation wrapper helpers.
packages/capacitor-plugin/src/index.ts Public exports for the package API surface.
packages/capacitor-plugin/src/connect.ts Wires @capacitor/app events to state.pause/resume and back-button forwarding.
packages/capacitor-plugin/src/bind-stage-back.ts Adds per-Stage lifecycle helper for hardware-back subscription.
packages/capacitor-plugin/src/back-event.ts Implements internal event-bus forwarding + subscription helpers.
packages/capacitor-plugin/scripts/clean.ts Adds build folder cleanup script.
packages/capacitor-plugin/scripts/build.ts Adds esbuild bundling script for ESM output + banner.
packages/capacitor-plugin/README.md Documents installation, quickstart, and API usage for the plugin.
packages/capacitor-plugin/package.json Adds package manifest (peers/devDeps/scripts/exports) for publishing.
packages/capacitor-plugin/LICENSE Adds package license file.
packages/capacitor-plugin/CHANGELOG.md Adds initial changelog entry for 1.0.0.
packages/capacitor-plugin/.gitignore Adds ignores for build artifacts and local files.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/capacitor-plugin/src/connect.ts Outdated
Comment thread packages/capacitor-plugin/src/bind-stage-back.ts
Comment thread packages/capacitor-plugin/package.json
Comment thread packages/capacitor-plugin/package.json
Comment thread packages/capacitor-plugin/src/types.ts Outdated
obiot and others added 2 commits May 10, 2026 10:47
Drop the legacy `boot()` + `video.init(...)` pair from the README's
quick-start example in favor of `new Application(width, height,
options)`, which is the modern entry point. The Application
constructor handles boot + renderer init in one call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five issues from Copilot, one real bug + four polish fixes:

- bind-stage-back: subscribe immediately, not just on the next reset.
  The documented call site is inside the stage's `onResetEvent`, so
  the wrapped reset method only runs on subsequent resets — leaving
  the handler silent for the current activation. Now subscribes
  eagerly, with the wrapper still re-subscribing idempotently across
  destroy/reset cycles.
- types: widen `onUnhandledBack` to `() => void | Promise<void>` so
  TypeScript accepts async handlers (e.g. confirm-dialog flows).
- connect: when a user-provided `onUnhandledBack` rejects or throws,
  emit a console.warn instead of letting it bubble as an unhandled
  rejection. Same treatment in `teardown()` for failed `h.remove()`
  calls.
- package.json: optional peer deps must appear in `peerDependencies`
  for `peerDependenciesMeta.optional` to actually take effect (npm
  ignores meta entries with no matching peer). Added
  `@capacitor/screen-orientation` and `@capacitor/splash-screen` to
  `peerDependencies` with the same `>=6.0.0` range as the required
  `@capacitor/app` peer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 05:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread packages/capacitor-plugin/src/connect.ts Outdated
Comment thread packages/capacitor-plugin/src/connect.ts
obiot and others added 2 commits May 10, 2026 15:50
Two follow-ups on PR #1439:

- Default `onUnhandledBack` now returns the `App.exitApp()` promise
  (`() => App.exitApp()`) instead of swallowing it via `void`. Same
  rejection-handling path as user-provided async handlers — a failed
  exit surfaces as a console.warn instead of an unhandled rejection.
- `connectCapacitor` returns an async teardown (`() => Promise<void>`)
  that awaits `Promise.allSettled(handles.map(h => h.remove()))`, so
  tests / hot-reload paths can `await cap.teardown()` for
  deterministic detachment. Calling without awaiting still works
  (fire-and-forget). `CapacitorPlugin.teardown` and the README's
  teardown blurb updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plugin doesn't actually depend on anything 19.x-specific. It
needs:
- `new Application(width, height, options)` as a standalone entry
  point (added in 18.3.0 — auto-calls `boot()`).
- `Stage.onResetEvent(app, ...args)` and `Stage.onDestroyEvent(app)`
  signatures with `app` as first parameter (added in 18.3.0).
- `plugin.BasePlugin`, `state.pause(boolean)` / `state.resume(boolean)`,
  the `event` namespace — all stable on 18.3.0.

Lowering the peer dep range from `>=19.3.0` to `>=18.3.0` makes the
plugin install for users still on 18.x. Matches the floor used by
other plugins in the monorepo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 07:59
The plugin's compatibility floor (`this.version` on `BasePlugin`)
was hardcoded as `"18.3.0"` in `plugin.ts`, duplicating the
`peerDependencies.melonjs` range in `package.json`. Bumping the floor
required two edits in two files and risked drift.

Now the build script reads `peerDependencies.melonjs`, strips the
range prefix (`>=`, `^`, `~`, ...), and injects the bare semver as
`__MELONJS_PEER__` via esbuild's `define`. `plugin.ts` references it
as `declare const` so tsc still type-checks the source. Verified the
bundle contains the substituted literal `"18.3.0"`.

Future floor bumps are now a one-line edit to `package.json`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +51 to +55
const origReset = stage.onResetEvent.bind(stage);
const origDestroy = stage.onDestroyEvent.bind(stage);
stage.onResetEvent = function (app: Application, ...args: unknown[]) {
subscribe();
origReset(app, ...args);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants