Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/smogon/pokemon-showdown-client/llms.txt

Use this file to discover all available pages before exploring further.

Contributing to the Pokémon Showdown client means writing modern ES2018 TypeScript that Babel 7 compiles all the way down to ES3 — the JavaScript dialect understood by Internet Explorer 7. This sounds limiting, but in practice Babel handles nearly everything automatically. There are just a handful of language features you must consciously avoid, a small set of polyfills you can rely on, and a few formatting conventions enforced by ESLint.

Compatibility Targets

The client has two distinct compatibility tiers:

Replay Player

Must support IE7 and later. The strictest tier. Avoid any runtime construct that does not compile to valid ES3.

Main Client

Must support IE9, Safari 5, and later. Slightly more relaxed, but still targets ES3 output through Babel.
Even outside the replay player, constructs like {return: 1} are not allowed because return is a reserved word in ES3. Always quote reserved words used as object keys: {"return": 1}.

Things You Cannot Use

Despite writing ES2018 syntax, a few features have no viable ES3 polyfill or produce unacceptable overhead when compiled. Avoid these entirely:
Map and Set can technically be polyfilled, but it is better to use plain objects directly.
// ❌ Do not use
const seen = new Map<string, number>();
const unique = new Set<string>();

// ✅ Use plain objects instead
const seen: {[key: string]: number} = Object.create(null);
const unique: {[key: string]: boolean} = Object.create(null);
Object.create(null) produces a prototype-free object, which avoids accidental collisions with inherited properties.
There is no way to compile async/await to ES3. Promises themselves are fine because they can be shimmed at runtime.
// ❌ Do not use
async function fetchData() {
  const result = await fetch(url);
  return result.json();
}

// ✅ Use Promises instead
function fetchData(): Promise<unknown> {
  return fetch(url).then(result => result.json());
}
Generator functions and non-Array iterables either produce enormous compiled output or are outright unsupported. for-of is allowed only on Arrays — Babel transforms it with zero overhead when it can assume Array.
// ❌ Do not use
function* range(n: number) { for (let i = 0; i < n; i++) yield i; }

// ✅ Use regular arrays and for-of (arrays only)
const items: string[] = ['a', 'b', 'c'];
for (const item of items) { /* ... */ }

Available Polyfills

The following APIs are shimmed at runtime. They are optimized for speed rather than strict spec-compliance — avoid edge cases documented below.
APICaveat
Array#includesWill not find NaN values
Array.isArray
String#startsWith
String#endsWith
String#includes
String#trim
Object.assign
Object.createSecond argument (property descriptors) is unsupported
Array#includes is added directly to Array.prototype. Never use for-in on an array — it will enumerate the polyfilled method. TypeScript will warn you if you try.

TypeScript Configuration

TypeScript is used throughout the codebase and is checked as part of npm run test. The tsconfig.json enables strict mode and a number of additional checks:
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "es6", "es2016.array.include", "es2017.object"],
    "noEmit": true,
    "target": "esnext",
    "module": "None",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "verbatimModuleSyntax": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "preserve",
    "strict": true,
    "noImplicitOverride": true
  }
}
noEmit: true means tsc is used only for type-checking. Babel handles the actual compilation to JavaScript. This separation allows Babel to target ES3 while TypeScript checks against modern typings.
Key implications of the TypeScript config:
  • strict: true — enables strictNullChecks, noImplicitAny, and related checks. All types must be explicit or inferrable.
  • noImplicitOverride: true — methods that override a base-class method must be marked with the override keyword.
  • verbatimModuleSyntax: true — type-only imports must use import type to allow Babel to strip them safely.
  • jsx: "preserve" — JSX is left as-is by tsc and transformed by Babel using the Preact pragma.

Babel Transform Pipeline

The .babelrc defines the full transform pipeline in order. Babel handles everything from TypeScript stripping to ES3 property literal quoting:
[
  ["@babel/plugin-transform-typescript", {"isTSX": true, "allowDeclareFields": true}],
  ["@babel/plugin-transform-react-jsx", {
    "pragma": "preact.h",
    "pragmaFrag": "preact.Fragment",
    "useBuiltIns": true
  }]
]
JSX is compiled to preact.h(...) calls — not React.createElement. Always import Preact when writing JSX.
[
  "@babel/plugin-transform-logical-assignment-operators",
  ["@babel/plugin-transform-nullish-coalescing-operator", {"loose": true}],
  ["@babel/plugin-transform-optional-chaining", {"loose": true}],
  ["@babel/plugin-transform-object-rest-spread", {"loose": true, "useBuiltIns": true}]
]
These transforms allow modern ??, ?., ??=, &&=, and ||= syntax in source while producing ES3-compatible output.
[
  "@babel/plugin-transform-arrow-functions",
  ["@babel/plugin-transform-block-scoping", {"throwIfClosureRequired": true}],
  ["@babel/plugin-transform-classes", {"loose": true}],
  ["@babel/plugin-transform-for-of", {"assumeArray": true}]
]
assumeArray: true is what makes for-of zero-overhead — it only works correctly when iterating Arrays. Non-Array iterables will silently break at runtime.
[
  "@babel/plugin-transform-member-expression-literals",
  "@babel/plugin-transform-property-literals"
]
These plugins automatically quote reserved words used as property names (e.g. converting obj.return to obj["return"]). Even so, be careful: write quoted keys in object literals yourself when using reserved words, since the linter will catch unquoted usage before Babel can fix it.

ESLint Configuration

ESLint is configured in eslint.config.mjs using the eslint-ps-standard shared config and @stylistic/eslint-plugin for formatting rules. Three distinct profiles are applied to different file groups:

ES3 (old client)

Files in play.pokemonshowdown.com/src/oldclient/. Uses configs.es3 with ecmaVersion: 3 and a large list of browser/battle globals.

JavaScript for Node

Build tools in build-tools/ and *.mjs files. Uses configs.js with Node globals.

TypeScript

All .ts and .tsx source files. Uses configs.es3ts with project-service type-aware linting.
Run the linter:
# Check only
npm run lint

# Auto-fix style issues
npm run fix
The test suite runs ESLint with --max-warnings 0, so every warning is treated as an error in CI.

Notable Rule Overrides

RuleSettingReason
prefer-constoff (TypeScript files)Temporarily disabled during the Preact rewrite
@stylistic/padded-blocksoffUsed intentionally for grouping related code
@typescript-eslint/no-floating-promisesoffToo many intentional fire-and-forget patterns in the client
@typescript-eslint/unbound-methodoffUsed intentionally for animation callbacks

JSX with Preact

The client uses Preactnot React. The JSX pragma is set to preact.h in .babelrc. Import Preact explicitly in every file that uses JSX:
import preact from '../js/lib/preact';

// JSX compiles to preact.h(...) calls
export function MyPanel() {
  return <div class="panel">Hello</div>;
}
Do not import from 'react'. There is no React in this codebase. JSX without a Preact import will produce a runtime ReferenceError: preact is not defined.

File Naming Conventions

Source files follow a consistent naming pattern that reflects their role in the loading phases:
PrefixRoleExamples
panel-UI panels (Preact components)panel-chat.tsx, panel-battle.tsx, panel-mainmenu.tsx
battle-Battle simulation and databattle-dex.ts, battle-animations.ts, battle-log.ts
client-Core client infrastructureclient-main.ts, client-connection.ts, client-core.ts
New files should follow this convention. TypeScript source goes in play.pokemonshowdown.com/src/ for the main client, replay.pokemonshowdown.com/src/ for the replay player, or teams.pokemonshowdown.com/src/ for the teams subsite.

Reserved Word Gotcha

This is worth calling out explicitly. ES3 treats many words as reserved even in property position. Babel’s property-literal transform handles compiled output, but your source code should still use quoted keys to keep the linter happy and intentions clear:
// ❌ Not allowed — 'return' is a reserved word in ES3
const config = {return: 1, delete: true};

// ✅ Always quote reserved words as object keys
const config = {"return": 1, "delete": true};
Common ES3 reserved words to watch out for: return, delete, default, class, extends, import, export, in, instanceof, typeof, void.

Build docs developers (and LLMs) love