[READ-ONLY] Mirror of https://github.com/excaliburjs/create-excalibur. A repo bootstrapping tool to get you started making your first games!
TypeScript 100%
JavaScript <1%
MDX <1%
<1%

README.md

create-excalibur #

Scaffolding for ExcaliburJS projects #

With NPM:

npx create-excalibur@latest

example running create excalibur

The ex CLI #

Installing the package globally also gives you the ex command:

npm i -g create-excalibur   # installs `ex` (and an `excalibur` alias)
ex            # interactive menu (same as create-excalibur)
ex create     # scaffold a game from a template
ex sample     # scaffold a sample project
ex inspect    # download a showcase game
ex docs       # search the Excalibur docs & API
ex generate   # generate an actor, label, scene, resource, engine settings, material, spritesheet, or animation — or update an actor's options (alias: ex g)
ex doctor     # type-aware diagnostics: actors never added to a scene, unnamed actors
ex upgrade    # migrate your game to a newer Excalibur version, codemod-style (alias: ex up)
ex mcp        # MCP server over stdio (docs + codegen tools for AI agents)

ex docs — search the docs from your terminal #

ex docs                         # type-as-you-search prompt
ex docs actor collision         # search, pick a result, read it in the terminal
ex docs Vector.distance -1      # open the top result immediately (no picker)
ex docs actor collision --list  # just print the matches + links (pipe friendly)
ex docs vector --json           # machine-readable results
ex docs --help                  # all options

Search is powered by the excaliburjs.com DocSearch (Algolia) index. Pages are rendered as markdown in the terminal with links back to the online docs; long pages open in $PAGER/less.

Version aware: when run inside a project, ex docs detects the installed excalibur version (from node_modules or package.json) and renders pages from that release's docs (--ref v0.32.0 to override; --ref main for the latest).

Offline:

ex docs offline            # download the docs for your Excalibur version (~1 MB) + plugin READMEs + build a local index
ex docs actor --offline    # search the downloaded docs only
ex docs offline --status   # what's cached and where (~/.excalibur/docs, or $EXCALIBUR_HOME)
ex docs offline --clear    # remove the cache

When the network is unavailable, ex docs falls back to the offline index automatically.

Plugins: ex docs offline also indexes the @excaliburjs/plugin-* READMEs (Tiled, Aseprite, LDtk, perlin, …) from npm, so plugin usage is searchable too — filter with --kind plugin.

ex doctor — check your game for common mistakes #

ex doctor           # human-readable report, exits 1 when problems are found
ex doctor --json    # machine-readable findings (CI friendly)

Type-aware diagnostics powered by your project's own TypeScript and excalibur's type declarations (run npm install first). Twelve rules, each grounded in bugs found in shipped games:

  • actor-not-added — an Actor-derived new that never reaches .add/.addChild
  • unnamed-actor — an Actor constructed without a name, harder to spot in debugging tools
  • dont-shadow-excalibur-internals — a field like isActive on an Entity subclass shadows engine state and silently kills the entity (tip: set "noImplicitOverride": true)
  • leaked-subscription — .on() to an engine-lifetime emitter with no cleanup; handlers compound across scene restarts
  • dead-collision-hooks — collision handlers while the Engine has physics: false
  • dont-mutate-shared-graphics — writes to cached getAnimation()/getSpriteSheet() results; .clone() first
  • unknown-scene-key — goToScene typos checked against the scenes: map
  • dont-call-lifecycle-hooks — calling an engine lifecycle hook directly instead of letting the engine invoke it
  • camera-pos-aliasing — camera.pos = actor.pos writes through to the live vector
  • no-reserved-tags — engine-owned ex.* tags added via addTag/removeTag
  • no-reserved-uniforms — a Material/ScreenShader source declares a built-in like u_time_ms or v_uv with a conflicting GLSL type; the engine sets it by name at draw time, so it silently reads as zeros or fails to link
  • prefer-seeded-random — Math.random(), unseeded new Random(), and duplicate seeds that correlate streams

Run ex doctor --help for the list. Doctor scans src/ plus any example/, examples/, demo/, sample/ or sandbox/ directory that exists (plugins keep their public-API usage there), or the package root when there is no src/; --scope <dir> (repeatable) overrides. prefer-seeded-random is also the cheapest pre-check before recording screenshot baselines — unseeded randomness is what makes them flaky.

Ignore a finding case-by-case with eslint-style comments — after a report, an interactive prompt offers to insert them for you:

// ex-doctor-ignore-next-line actor-not-added
new OffscreenHelper();
new Cursor(); // ex-doctor-ignore-line unnamed-actor

Omit the rule list to ignore every rule on that line.

ex upgrade — codemod-style version migrations #

ex upgrade --dry-run       # preview the full migration plan, write nothing
ex upgrade                 # plan preview + one confirm, then apply + bump package.json
ex upgrade --to next       # target v1 (the `next` prerelease); default is latest
ex upgrade --migrate-only  # rewrite code but leave package.json alone

Chained migrations (v0.29.3 onward, ng-update style) rewrite your source with formatting-preserving splices, classified against your project's installed excalibur types — so run it before installing the new version. Every migration is one of three types:

Automated — rewritten for you, no review needed:

  • ex.Input.* namespace flattened into ex.*
  • event .delta → .elapsed
  • Engine.goto(...) → goToScene(...)
  • GraphicsComponent.show(...) → use(...)
  • Vector.size → magnitude
  • getGlobalPos()/getGlobalRotation()/getGlobalScale() → the equivalent accessors
  • EventDispatcher → EventEmitter
  • Engine.get/setAntialiasing() → engine.screen.antialiasing
  • Particle/ParticleEmitter option renames (emitters gained a nested particle config)
  • easeTo/easeBy actions → moveTo/moveBy with easing
  • Timer now only takes the option-bag constructor
  • ScreenShader v_texcoord → v_uv
  • BoundingBox.draw(...) → debug(...)
  • TileMap's default compositeStrategy changed to 'separate' — pins the old default

Manual — needs human judgment, so // ex-upgrade(<id>): … breadcrumb comments are inserted at each site with a link and recipe:

  • ex.Physics.* statics were removed — configure physics in the Engine constructor
  • collision events now target Collider (was sometimes Entity)
  • System.priority is now static
  • Trigger API changed (action signature, target vs filter)
  • legacy EasingFunctions.* are deprecated — use the simple (t) => number forms
  • screen space is now rooted at the content area (v1's worldToScreenCoordinates/contentArea change)

Notification — no code changes, just a heads-up about a behavior change:

  • Vector.normalize() on a zero vector now returns (0,0) (was (0,1))
  • realistic physics bodies now sleep by default
  • Font/Text render slightly differently in v1

Requires a clean git tree (your undo) unless --allow-dirty; never runs npm install for you.

ex mcp — MCP server for AI agents #

Exposes the CLI's capabilities as Model Context Protocol tools over stdio, so agents like Claude Code and OpenCode can search the Excalibur docs, scaffold projects, and generate code in your project.

Claude Code (add -s user to register it globally instead of per-project):

claude mcp add excalibur -- npx -y create-excalibur mcp

OpenCode — add to opencode.json in your project (or ~/.config/opencode/opencode.json):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "excalibur": {
      "type": "local",
      "command": ["npx", "-y", "create-excalibur", "mcp"],
      "enabled": true
    }
  }
}
ex mcp                      # serve, tools operate on the current directory by default
ex mcp --project <dir>      # point the tools at a specific project
ex mcp --help

Docs — also cover the @excaliburjs/plugin-* READMEs (kind: "plugin", /plugins/<name> slugs):

  • docs_search — search guides, API reference, and plugin READMEs; live by default, falls back to the offline cache
  • docs_get_page — fetch a docs page or plugin README (or one section) as markdown
  • docs_sync — download docs + plugin READMEs into the local cache for offline/version-pinned search

Generate — scaffold new code into the project (accept dryRun to preview):

  • analyze_project — inspect scenes, actors, resources, spritesheets, installed version/plugins
  • generate_actor — new Actor class, optionally wired into a scene
  • generate_label — new Label (text) class, optionally wired into a scene
  • generate_scene — new Scene class, registered in the scenes map by default
  • generate_resource — register an image/sound/font/other asset in the resource loader
  • generate_material — new WebGL shader Material, optionally assigned to an actor
  • generate_spritesheet — slice a sheet image into an ex.SpriteSheet
  • generate_animation — build an ex.Animation from an existing spritesheet

Update — edit existing code in place, preserving untouched options and comments:

  • update_actor — change an Actor's super({ ... }) ActorArgs
  • update_engine — change the project's new Engine(...) options

Project lifecycle:

  • list_templates — list templates and sample projects usable with create_project
  • create_project — scaffold a new game from a template (skips npm install/git init unless asked)

Diagnostics:

  • doctor — type-aware lint for common Excalibur mistakes (14 rules); scope narrows the scan
  • preflight — read-only readiness check before upgrade: git state, where excalibur is declared (incl. peerDependencies), whether its types resolve, scan roots, what to resolves to, and how many migrations are in the path
  • upgrade — chained codemod migrations to a newer Excalibur version. to: "next" resolves the dist-tag to an exact version and pins it (a floating tag makes installs non-reproducible); libraries get peerDependencies.excalibur bumped too; the result's backlog lists older migrations the code still matches (re-run with from); include runs a single migration id, scope narrows the scan

Errors come back with actionable hints so agents can self-correct.

Note: ex shadows the rarely-used system ex (vi's line-editor mode) while the npm global bin dir is first on your PATH. Use the excalibur alias if that bothers you.

Architecture #

How the pieces fit together. Everything is TypeScript ESM; each command is a "flow" registered in src/constants.ts and dispatched from index.ts. Development runs the sources directly (Node's type stripping — no build step in the dev loop); publishing compiles to dist/ via tsc, which is what the bins run on end-user machines.

Command dispatch #

Both bins point at the compiled dist/index.js. Dispatch is persona-aware: the create persona treats a bare positional as a project name, while ex/excalibur stay strict so a typo never scaffolds.

flowchart LR
    A["npm create excalibur my-game"] --> D
    B["create-excalibur bin"] --> D
    C["ex / excalibur bins"] --> D
    D["resolveInvocation<br/>src/dispatch.ts"]
    D -->|"no args"| MENU["interactive menu<br/>FLOW_CHOICES"]
    D -->|"known command"| FLOWS["FLOWS lookup<br/>src/constants.ts"]
    D -->|"create persona + positional"| CREATE["create flow<br/>name pre-filled"]
    D -->|"ex persona + unknown"| ERR["error: unknown command"]
    MENU --> FLOWS
    FLOWS --> F1["create / sample / inspect"]
    FLOWS --> F2["docs"]
    FLOWS --> F3["generate"]
    FLOWS --> F4["doctor"]
    FLOWS --> F5["mcp<br/>dynamic import, stdout = protocol only"]
    F5 -.->|"16 tools reuse the same cores:<br/>search, apply, doctor, scaffold"| F2

ex docs — search and the offline index #

Searches hit the site's Algolia index first and fall back to a locally built index; ex docs offline builds that index straight from the Excalibur repo's docs source, pinned to your installed version.

flowchart TD
    Q["ex docs query"] --> RS["runDocsSearch<br/>src/docs/search.ts"]
    RS -->|"online"| ALG["Algolia DocSearch<br/>public search-only key"]
    RS -->|"--offline"| LOCAL["MiniSearch index<br/>one doc per page section"]
    ALG -->|"network error"| LOCAL
    ALG --> MERGE["merge plugin README hits<br/>up to 3 tail slots"]
    LOCAL --> MERGE
    MERGE --> RENDER["markdown to ANSI renderer<br/>pager for long pages"]

    subgraph SYNC["ex docs offline — sync and indexing"]
        V["detect installed excalibur<br/>node_modules or package.json"] --> REF["pick ref: release tag v0.32.0<br/>or main for old/no version"]
        REF --> TREE["GitHub trees API, one call<br/>list site/docs/**"]
        TREE --> RAW["fetch raw files by commit sha<br/>raw.githubusercontent.com"]
        RAW --> MDX["mdx.ts: frontmatter slugs, admonitions,<br/>playground embeds, wiki links"]
        MDX --> IDX["cache ~/.excalibur/docs/ref:<br/>index.json + slugs.json + manifest"]
        NPM["npm registry:<br/>@excaliburjs/plugin-* readmes"] --> PIDX["plugin index<br/>sibling plugins/ cache"]
        ALG2["Algolia symbol sweep"] --> SYM["api-symbols.json<br/>resolves wiki links"]
    end
    IDX --> LOCAL
    PIDX --> MERGE

ex generate — what it looks for in your TypeScript #

Generation is a wizard/apply split: the wizard only builds an option model, and apply*() does the edits. Edits are minimal text splices validated by re-parsing — never a full AST reprint, so your formatting and comments survive. It uses your project's own TypeScript (never bundled; TypeScript 7 removed the compiler API, so it asks for 5.x/6.x).

flowchart TD
    G["ex generate actor / label / scene / resource /<br/>engine / material / update-actor / spritesheet / animation"] --> AP["analyzeProject"]
    AP --> TSL["load the project's TypeScript<br/>from its node_modules"]
    AP --> SCAN
    subgraph SCAN["syntactic AST scan of src/**/*.ts"]
        S1["new Engine and its<br/>scenes map keys"]
        S2["Resources literal:<br/>keys + asset paths"]
        S3["classes extending Scene"]
        S4["classes extending<br/>Actor / Label / ScreenElement"]
        S5["SpriteSheet consts:<br/>grid, spacing, image key"]
        S6["package.json:<br/>@excaliburjs/* plugins"]
    end
    SCAN --> W["wizard prompts<br/>build an option model"]
    W --> APPLY["apply: minimal text splices<br/>insert option property, add import,<br/>add to a scene's onInitialize"]
    APPLY --> VAL["re-parse: zero syntax errors<br/>or the edit is abandoned"]
    VAL -->|"ok"| WRITE["write files"]
    VAL -->|"seam not found"| MANUAL["print a manual snippet<br/>instead of guessing"]

ex doctor — type-aware diagnostics #

Doctor is the one place a full ts.Program + TypeChecker is used (generate stays syntactic): the checker is what catches class Boss extends Monster extends Actor. Rules are kind-keyed listeners over a single AST walk per file, the same shape typescript-eslint uses.

flowchart LR
    D["ex doctor --json"] --> AP["analyzeProject"]
    AP --> PROG["ts.createProgram + TypeChecker<br/>tsconfig.json or vite-flavored defaults"]
    PROG --> PROBE["probe: excalibur .d.ts resolvable?<br/>no: run npm install"]
    PROBE --> WALK["one AST walk per src file<br/>dispatch to rule listeners"]
    WALK --> R1["actor-not-added:<br/>Actor-derived new never traced<br/>to add or addChild"]
    WALK --> R2["unnamed-actor:<br/>super options or new Actor<br/>without a name"]
    R1 --> OUT["findings: rule, file:line, message, hint<br/>exit 1 when any are found"]
    R2 --> OUT

Running this project locally #

Run npm run dev, or node index.ts docs <query> (Node 22.18+/24 — the dev loop runs the TypeScript sources directly via type stripping; end users only ever run compiled JS).

Tests: npm test · Typecheck: npm run typecheck · Build: npm run build · Publish smoke test: npm run smoke:pack