Phaser + Vite Setup: A Real Project Structure (2026)
Phaser + Vite Setup: A Real Project Structure (2026)
Honesty note: this is my actual Phaser project setup (Merge Fish 2048) — the
vite.config.js,package.json, and scene structure I ship with. My project is JavaScript; I’ll show how to add TypeScript to the same setup, marked clearly as the optional path.
TL;DR
- Vite + Phaser is the lowest-friction HTML5 game stack in 2026: instant dev server,
npm run build→ adist/folder you can deploy anywhere (Cloudflare Pages, any static host, any portal). - Three Vite settings matter for games:
base: './'(relative paths so the build works in any iframe/subfolder),assetsInlineLimit: 0(keep small assets as files, not inlined base64), andmanualChunksto split the Phaser library so your game code updates don’t redownload the engine (see the size guide). - Structure by responsibility:
scenes/(UI flows),game/(pure logic — board, levels, catalog),utils/(storage, audio, ads, buttons). Logic separate from rendering = testable and AI-friendly (AI workflow). - TypeScript is optional and cheap to add: install
typescript, add atsconfig.json(strict,moduleResolution: bundler), rename.js→.ts. Phaser ships full TS types. The rest of the stack is unchanged.
The real config
// vite.config.js — my exact file
import { defineConfig } from 'vite';
export default defineConfig({
base: './', // relative asset paths — works in iframes/subfolders
build: {
outDir: 'dist',
assetsInlineLimit: 0, // keep assets as separate files (no base64 bloat)
rollupOptions: {
output: {
manualChunks: {
phaser: ['phaser'] // Phaser in its own chunk — cacheable across builds
}
}
}
},
server: {
port: 5173,
open: true // auto-open browser on dev start
}
});
// package.json — the essentials
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 8080"
},
"dependencies": { "phaser": "^3.90.0" },
"devDependencies": { "vite": "^5.4.0" }
}
Three choices worth copying:
base: './'— the build uses relative paths, sodist/works when dropped into any portal iframe or subdirectory. Without it, absolute paths break your game off your own domain.assetsInlineLimit: 0— Vite normally inlines small assets as base64; for games that bloats the JS bundle. Keep them as files.manualChunks: { phaser: ['phaser'] }— Phaser (~1.3 MB min) lives in its own chunk; your code changes rebuild only your chunk, and browsers cache the engine across deploys.
The scene structure that scales
src/
├── main.js # Phaser.Game config + scene registration
├── config.js # game-wide constants (dimensions, physics, storage keys)
├── scenes/ # UI flows — Boot, Menu, Help, LevelSelect, Game, Aquarium, Rank, Settings, GameOver
├── game/ # PURE logic, no Phaser imports — Board, FishCatalog, LevelConfig
└── utils/ # Phaser-touching helpers — Storage, AudioManager, RewardManager, ButtonFactory
The rule that keeps it maintainable: game/ never imports Phaser. Board logic (merge rules, win/lose checks) is plain JavaScript — testable in a headless run, portable to another engine if you ever switch, and fully visible to AI tools. scenes/ and utils/ are the Phaser-facing layer. (My 9-scene structure is covered in more depth in the scene management guide.)
Adding TypeScript (the optional path)
My project is JavaScript, but the same setup takes TS in three steps:
npm i -D typescript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"include": ["src"]
}
Then rename .js → .ts — Phaser ships complete type definitions, so you get autocomplete on this.physics, this.add, scene lifecycle, etc. The Vite config and build pipeline are unchanged; Vite handles TS transpilation natively.
Why I stayed on JS: my project is small, the types would help most in game/ (which is pure logic I test headlessly anyway), and the AI-assisted workflow reads plain JS just as well. If your game grows past a few scenes or you share code with teammates, TS is the right call.
What this produces
npm run build → a dist/ folder with:
assets/— your textures, audio (separate files, not inlined)assets/phaser-*.js— the engine chunk (cached across deploys)index.html+ your game chunk
That folder deploys to Cloudflare Pages, GitHub Pages, any static host, or straight into a portal iframe. The asset pipeline feeds it game-ready assets; the marketing guide explains where to put the result.
Pitfalls
- Forgetting
base: './'— the #1 “works locally, breaks deployed” bug for games in iframes/subfolders. - Inline everything — small PNGs become base64 in the JS bundle; you lose HTTP caching and bloat the entry file.
- Mixing logic with scenes — board rules inside a scene = untestable, AI-hostile, and painful to port.
- TS without
moduleResolution: bundler— Vite-based projects need the bundler resolution; the classic “cannot find module ‘phaser’” trap. - No
.gitignorefornode_modules/dist— you’ll push megabytes of junk to the repo (the deploy pipeline builds it fresh anyway).
Bottom line
Phaser + Vite is the pragmatic 2026 web-game stack: three Vite settings (base, assetsInlineLimit, manualChunks) make the build portable and lean; a responsibility-split structure (pure logic vs Phaser layer) keeps it maintainable and AI-friendly; TypeScript is a three-step optional addition with full Phaser types. This exact setup ships my game today — npm run build → dist/ → deployed, no engine binary, ~1-2 MB gzipped (why that matters).