Project Structure
Here's what each file in the template does.
├── src/
│ ├── main.ts # Your mod's entry point
│ ├── ui/
│ │ └── ExamplePanel.tsx # Example React component
│ └── types/
│ ├── react.ts # React shim (pulls React from game API)
│ ├── index.d.ts # Re-exports + global Window declarations
│ ├── api.d.ts # Main ModdingAPI interface
│ ├── core.d.ts # Coordinate, BoundingBox, GameSpeed
│ ├── game-state.d.ts # Station, Track, Train, Route types
│ ├── game-constants.d.ts# GameConstants, ConstructionCosts
│ ├── game-actions.d.ts # Bond, BondType types
│ ├── build.d.ts # Build automation types
│ ├── ui.d.ts # UI placements & option types
│ ├── cities.d.ts # City, CityConfig types
│ ├── trains.d.ts # TrainTypeConfig, TrainTypeStats
│ ├── stations.d.ts # StationTypeConfig
│ ├── map.d.ts # Map sources, layers, overrides
│ ├── career.d.ts # MissionConfig, StarConfig
│ ├── content-templates.d.ts # Newspaper & tweet templates
│ ├── pop-timing.d.ts # CommuteTimeRange
│ ├── i18n.d.ts # I18nAPI
│ ├── utils.d.ts # RechartsComponents
│ ├── schemas.d.ts # Zod validation schemas
│ ├── electron.d.ts # ElectronAPI types
│ └── manifest.d.ts # ModManifest type
├── scripts/
│ ├── run.ts # Game launcher with logging
│ └── link.ts # Symlink management
├── manifest.json # Mod metadata (loaded by the game)
├── vite.config.ts # Build configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies and scripts
Key Files
manifest.json
The game reads this to identify your mod. Required fields:
{
"id": "com.author.modname",
"name": "My Mod",
"description": "Description of your mod",
"version": "1.0.0",
"author": { "name": "Your Name" },
"main": "index.js"
}
id— unique identifier in reverse-domain notationmain— always"index.js"(the Vite build output)
src/main.ts
Your mod's entry point. This is where you register hooks, add UI elements, and set up your mod's logic. The template comes with a working example:
const api = window.SubwayBuilderAPI;
if (!api) {
console.error('SubwayBuilderAPI not found!');
} else {
let initialized = false;
api.hooks.onMapReady((_map) => {
if (initialized) return;
initialized = true;
// Set up your mod here
api.ui.addFloatingPanel({
id: 'my-mod-panel',
title: 'My Mod',
icon: 'Puzzle',
render: ExamplePanel,
});
});
}
src/ui/ExamplePanel.tsx
A sample React component that demonstrates how to use game UI components and hooks inside a floating panel:
import { useState } from 'react';
const api = window.SubwayBuilderAPI;
const { Button } = api.utils.components as Record<string, React.ComponentType<any>>;
export function ExamplePanel() {
const [count, setCount] = useState(0);
return (
<div className="flex flex-col gap-3 p-3">
<p className="text-sm text-muted-foreground">Click count: {count}</p>
<Button onClick={() => setCount((c) => c + 1)}>Increment</Button>
</div>
);
}
src/types/react.ts
The React shim. This is how JSX works in mods — instead of bundling React, the shim pulls React from the game's API at runtime:
const React = window.SubwayBuilderAPI.utils.React;
export default React;
export const { useState, useEffect, useCallback, useMemo, useRef /* ... */ } = React;
// JSX runtime exports
export const jsx = React.createElement;
export const jsxs = React.createElement;
Vite is configured to alias react and react/jsx-runtime imports to this file, so you can write
standard import { useState } from 'react' and it just works.
vite.config.ts
The build configuration. Key points:
- Output format: IIFE (immediately-invoked function expression) — the game expects a single script, not ES modules
- React aliasing: Routes
reactimports through the shim - Static copy: Copies
manifest.jsonintodist/alongside the built JS - No minification: Keeps the output readable for debugging
tsconfig.json
Standard TypeScript config with:
strict: true— full type safetyjsx: "react-jsx"— automatic JSX transformnoEmit: true— TypeScript is only used for type checking, Vite handles the actual build
scripts/link.ts
Creates a symlink from dist/ to the game's mods folder. It reads the mod ID from manifest.json
and uses the last segment as the folder name (e.g., com.author.mymod becomes mymod/).
scripts/run.ts
Finds the Subway Builder executable on your system and launches it with ELECTRON_ENABLE_LOGGING=1.
Captures stdout/stderr to debug/latest.log so you can review console output after the session.