Enable @total-typescript/ts-reset (#1761)

## Description:

Enable `@total-typescript/ts-reset`

Fixes #1760

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [ ] I have read and accepted the CLA agreement (only required once).
This commit is contained in:
Scott Anderson
2025-08-09 02:13:56 -04:00
committed by GitHub
parent 2b0701c132
commit 7e25f6b910
31 changed files with 248 additions and 76 deletions
+8
View File
@@ -43,6 +43,7 @@
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@swc/jest": "^0.2.39",
"@total-typescript/ts-reset": "^0.6.1",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
@@ -6604,6 +6605,13 @@
"@swc/counter": "^0.1.3"
}
},
"node_modules/@total-typescript/ts-reset": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz",
"integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+1
View File
@@ -31,6 +31,7 @@
"@eslint/js": "^9.21.0",
"@swc/jest": "^0.2.39",
"@types/benchmark": "^2.1.5",
"@total-typescript/ts-reset": "^0.6.1",
"@types/chai": "^4.3.17",
"@types/d3": "^7.4.3",
"@types/express": "^4.17.23",
+1 -1
View File
@@ -229,7 +229,7 @@ export class ClientGameRunner {
players,
// Not saving turns locally
[],
startTime(),
startTime() ?? 0,
Date.now(),
update.winner,
this.lobby.serverConfig,
+14 -2
View File
@@ -1,4 +1,8 @@
import { UserMeResponse } from "../core/ApiSchemas";
import { z } from "zod";
import {
StripeCreateCheckoutSessionResponseSchema,
UserMeResponse,
} from "../core/ApiSchemas";
import { Cosmetics, CosmeticsSchema, Pattern } from "../core/CosmeticSchemas";
import { getApiBase, getAuthHeader } from "./jwt";
@@ -59,7 +63,15 @@ export async function handlePurchase(priceId: string) {
return;
}
const { url } = await response.json();
const json = await response.json();
const parsed = StripeCreateCheckoutSessionResponseSchema.safeParse(json);
if (!parsed.success) {
const error = z.prettifyError(parsed.error);
console.error("Invalid checkout session response:", error);
alert("Checkout failed. Please try again later.");
return;
}
const { url } = parsed.data;
// Redirect to Stripe checkout
window.location.href = url;
+1 -1
View File
@@ -159,7 +159,7 @@ export class InputHandler {
groundAttack: "KeyG",
modifierKey: "ControlLeft",
altKey: "AltLeft",
...JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}"),
...(JSON.parse(localStorage.getItem("settings.keybinds") ?? "{}") ?? {}),
};
// Mac users might have different keybinds
+10 -6
View File
@@ -1,8 +1,12 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import { GameInfo, GameRecord } from "../core/Schemas";
import { GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
import {
WorkerApiArchivedGameLobbySchema,
WorkerApiGameIdExistsSchema,
} from "../core/WorkerSchemas";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { JoinLobbyEvent } from "./Main";
import "./components/baseComponents/Button";
@@ -198,7 +202,8 @@ export class JoinPrivateLobbyModal extends LitElement {
headers: { "Content-Type": "application/json" },
});
const gameInfo = await response.json();
const json = await response.json();
const gameInfo = WorkerApiGameIdExistsSchema.parse(json);
if (gameInfo.exists) {
this.message = translateText("private_lobby.joined_waiting");
@@ -231,7 +236,8 @@ export class JoinPrivateLobbyModal extends LitElement {
headers: { "Content-Type": "application/json" },
});
const archiveData = await archiveResponse.json();
const json = await archiveResponse.json();
const archiveData = WorkerApiArchivedGameLobbySchema.parse(json);
if (
archiveData.success === false &&
@@ -247,13 +253,11 @@ export class JoinPrivateLobbyModal extends LitElement {
}
if (archiveData.exists) {
const gameRecord = archiveData.gameRecord as GameRecord;
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
gameID: lobbyId,
gameRecord: gameRecord,
gameRecord: archiveData.gameRecord,
clientID: generateID(),
} as JoinLobbyEvent,
bubbles: true,
+25 -10
View File
@@ -1,19 +1,34 @@
import { GameConfig, GameID, GameRecord } from "../core/Schemas";
import { z } from "zod";
import {
GameConfig,
GameConfigSchema,
GameID,
GameRecord,
GameRecordSchema,
ID,
} from "../core/Schemas";
import { replacer } from "../core/Util";
export interface LocalStatsData {
[key: GameID]: {
lobby: Partial<GameConfig>;
const LocalStatsDataSchema = z.record(
ID,
z.object({
lobby: GameConfigSchema.partial(),
// Only once the game is over
gameRecord?: GameRecord;
};
}
gameRecord: GameRecordSchema.optional(),
}),
);
type LocalStatsData = z.infer<typeof LocalStatsDataSchema>;
let _startTime: number;
let _startTime: number | undefined;
function getStats(): LocalStatsData {
const statsStr = localStorage.getItem("game-records");
return statsStr ? JSON.parse(statsStr) : {};
try {
return LocalStatsDataSchema.parse(
JSON.parse(localStorage.getItem("game-records") ?? "{}"),
);
} catch (e) {
return {};
}
}
function save(stats: LocalStatsData) {
+9 -2
View File
@@ -1,3 +1,10 @@
import { z } from "zod";
const LockSchema = z.object({
owner: z.string(),
timestamp: z.number(),
});
export class MultiTabDetector {
private readonly tabId = `${Date.now()}-${Math.random()}`;
private readonly lockKey = "multi-tab-lock";
@@ -60,7 +67,7 @@ export class MultiTabDetector {
if (e.key === this.lockKey && e.newValue) {
let other: { owner: string; timestamp: number };
try {
other = JSON.parse(e.newValue);
other = LockSchema.parse(JSON.parse(e.newValue));
} catch (e) {
console.error("Failed to parse lock", e);
return;
@@ -99,7 +106,7 @@ export class MultiTabDetector {
const raw = localStorage.getItem(this.lockKey);
if (!raw) return null;
try {
return JSON.parse(raw);
return LockSchema.parse(JSON.parse(raw));
} catch (e) {
console.error("Failed to parse lock", raw, e);
return null;
+3 -1
View File
@@ -1,6 +1,7 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { translateText } from "../client/Utils";
import { ApiPublicLobbiesResponseSchema } from "../core/ExpressSchemas";
import { GameMapType, GameMode } from "../core/game/Game";
import { GameID, GameInfo } from "../core/Schemas";
import { generateID } from "../core/Util";
@@ -77,7 +78,8 @@ export class PublicLobby extends LitElement {
const response = await fetch(`/api/public_lobbies`);
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
const json = await response.json();
const data = ApiPublicLobbiesResponseSchema.parse(json);
return data.lobbies;
} catch (error) {
console.error("Error fetching lobbies:", error);
+4 -1
View File
@@ -1,5 +1,6 @@
import { LitElement, html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { z } from "zod";
import { translateText } from "../client/Utils";
import { UserSettings } from "../core/game/UserSettings";
import "./components/baseComponents/setting/SettingKeybind";
@@ -8,6 +9,8 @@ import "./components/baseComponents/setting/SettingNumber";
import "./components/baseComponents/setting/SettingSlider";
import "./components/baseComponents/setting/SettingToggle";
const KeybindSchema = z.record(z.string(), z.string());
@customElement("user-setting")
export class UserSettingModal extends LitElement {
private userSettings: UserSettings = new UserSettings();
@@ -25,7 +28,7 @@ export class UserSettingModal extends LitElement {
const savedKeybinds = localStorage.getItem("settings.keybinds");
if (savedKeybinds) {
try {
this.keybinds = JSON.parse(savedKeybinds);
this.keybinds = KeybindSchema.parse(JSON.parse(savedKeybinds));
} catch (e) {
console.warn("Invalid keybinds JSON:", e);
}
+17
View File
@@ -1,3 +1,4 @@
// This file contains schemas for api.openfront.io
import { z } from "zod";
import { base64urlToUuid } from "./Base64";
@@ -48,3 +49,19 @@ export const UserMeResponseSchema = z.object({
}),
});
export type UserMeResponse = z.infer<typeof UserMeResponseSchema>;
export const StripeCreateCheckoutSessionResponseSchema = z.object({
id: z.string(),
object: z.literal("checkout.session"),
url: z.string(),
payment_status: z.enum(["paid", "unpaid", "no_payment_required"]),
status: z.enum(["open", "complete", "expired"]),
client_reference_id: z.string().optional(),
customer: z.string().optional(),
payment_intent: z.string().optional(),
subscription: z.string().optional(),
metadata: z.partialRecord(z.string(), z.string()),
});
export type StripeCreateCheckoutSessionResponse = z.infer<
typeof StripeCreateCheckoutSessionResponseSchema
>;
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod/v4";
import { z } from "zod";
import { RequiredPatternSchema } from "./Schemas";
export const ProductSchema = z.object({
+15
View File
@@ -0,0 +1,15 @@
// This file contians schemas for the primary openfront express server
import { z } from "zod";
import { GameInfoSchema } from "./Schemas";
export const ApiEnvResponseSchema = z.object({
game_env: z.string(),
});
export type ApiEnvResponse = z.infer<typeof ApiEnvResponseSchema>;
export const ApiPublicLobbiesResponseSchema = z.object({
lobbies: GameInfoSchema.array(),
});
export type ApiPublicLobbiesResponse = z.infer<
typeof ApiPublicLobbiesResponseSchema
>;
+16 -11
View File
@@ -112,17 +112,6 @@ export type Player = z.infer<typeof PlayerSchema>;
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
const PlayerTypeSchema = z.enum(PlayerType);
export interface GameInfo {
gameID: GameID;
clients?: ClientInfo[];
numClients?: number;
msUntilStart?: number;
gameConfig?: GameConfig;
}
export interface ClientInfo {
clientID: ClientID;
username: string;
}
export enum LogSeverity {
Debug = "DEBUG",
Info = "INFO",
@@ -192,6 +181,22 @@ export const ID = z
export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
export const UsernameSchema = SafeString;
export const ClientInfoSchema = z.object({
clientID: ID,
username: UsernameSchema,
});
export type ClientInfo = z.infer<typeof ClientInfoSchema>;
export const GameInfoSchema = z.object({
clients: ClientInfoSchema.array().optional(),
gameConfig: GameConfigSchema.optional(),
gameID: ID,
msUntilStart: z.number().int().nonnegative().optional(),
numClients: z.number().int().nonnegative().optional(),
});
export type GameInfo = z.infer<typeof GameInfoSchema>;
const countryCodes = countries.map((c) => c.code);
export const FlagSchema = z
.string()
+32 -1
View File
@@ -1,5 +1,6 @@
// This file contians schemas for the openfront worker express server
import { z } from "zod";
import { GameConfigSchema } from "./Schemas";
import { GameConfigSchema, GameRecordSchema } from "./Schemas";
export const CreateGameInputSchema = GameConfigSchema.or(
z
@@ -9,3 +10,33 @@ export const CreateGameInputSchema = GameConfigSchema.or(
);
export const GameInputSchema = GameConfigSchema.partial();
export const WorkerApiGameIdExistsSchema = z.object({
exists: z.boolean(),
});
export type WorkerApiGameIdExists = z.infer<typeof WorkerApiGameIdExistsSchema>;
export const WorkerApiArchivedGameLobbySchema = z.union([
z.object({
error: z.literal("Game not found"),
exists: z.literal(false),
success: z.literal(false),
}),
z.object({
details: z.object({
actualCommit: z.string(),
expectedCommit: z.string(),
}),
error: z.literal("Version mismatch"),
exists: z.literal(true),
success: z.literal(false),
}),
z.object({
exists: z.literal(true),
gameRecord: GameRecordSchema,
success: z.literal(true),
}),
]);
export type WorkerApiArchivedGameLobby = z.infer<
typeof WorkerApiArchivedGameLobbySchema
>;
+3 -1
View File
@@ -1,3 +1,4 @@
import { ApiEnvResponseSchema } from "../ExpressSchemas";
import { UserSettings } from "../game/UserSettings";
import { GameConfig } from "../Schemas";
import { Config, GameEnv, ServerConfig } from "./Config";
@@ -36,7 +37,8 @@ export async function getServerConfigFromClient(): Promise<ServerConfig> {
`Failed to fetch server config: ${response.status} ${response.statusText}`,
);
}
const config = await response.json();
const json = await response.json();
const config = ApiEnvResponseSchema.parse(json);
// Log the retrieved configuration
console.log("Server config loaded:", config);
+2 -1
View File
@@ -1,5 +1,6 @@
import { GameMapType } from "./Game";
import { GameMapLoader, MapData } from "./GameMapLoader";
import { MapManifestSchema } from "./TerrainMapLoader";
export class FetchGameMapLoader implements GameMapLoader {
private maps: Map<GameMapType, MapData>;
@@ -66,6 +67,6 @@ export class FetchGameMapLoader implements GameMapLoader {
throw new Error(`Failed to load ${url}: ${response.statusText}`);
}
return response.json();
return response.json().then(MapManifestSchema.parse);
}
}
+21 -17
View File
@@ -1,3 +1,4 @@
import { z } from "zod";
import { GameMapType } from "./Game";
import { GameMap, GameMapImpl } from "./GameMap";
import { GameMapLoader } from "./GameMapLoader";
@@ -10,25 +11,28 @@ export type TerrainMapData = {
const loadedMaps = new Map<GameMapType, TerrainMapData>();
export interface MapMetadata {
width: number;
height: number;
num_land_tiles: number;
}
export const MapMetadataSchema = z.object({
height: z.number(),
num_land_tiles: z.number(),
width: z.number(),
});
export type MapMetadata = z.infer<typeof MapMetadataSchema>;
export interface MapManifest {
name: string;
map: MapMetadata;
mini_map: MapMetadata;
nations: Nation[];
}
export const NationSchema = z.object({
coordinates: z.tuple([z.number(), z.number()]),
flag: z.string(),
name: z.string(),
strength: z.number(),
});
export type Nation = z.infer<typeof NationSchema>;
export interface Nation {
coordinates: [number, number];
flag: string;
name: string;
strength: number;
}
export const MapManifestSchema = z.object({
map: MapMetadataSchema,
mini_map: MapMetadataSchema,
name: z.string(),
nations: NationSchema.array(),
});
export type MapManifest = z.infer<typeof MapManifestSchema>;
export async function loadTerrainMap(
map: GameMapType,
+6 -1
View File
@@ -144,7 +144,12 @@ export class UnitGrid {
searchRange,
);
const rangeSquared = searchRange * searchRange;
const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]);
const typeSet = new Set(
// Using typeof check instead of Array.isArray due to a typescript
// narrowing limitation. For more information, see the full issue
// discussion at https://github.com/mattpocock/ts-reset/issues/48
typeof types === "object" ? types : [types],
);
for (let cy = startGridY; cy <= endGridY; cy++) {
for (let cx = startGridX; cx <= endGridX; cx++) {
for (const type of typeSet) {
+1
View File
@@ -0,0 +1 @@
import "@total-typescript/ts-reset";
+9 -4
View File
@@ -1,6 +1,7 @@
import { spawn } from "child_process";
import { promises as fs } from "fs";
import yaml from "js-yaml";
import { z } from "zod";
import { logger } from "./Logger";
const log = logger.child({
@@ -41,6 +42,12 @@ interface CloudflaredConfig {
}>;
}
const CloudflareTunnelConfigSchema = z.object({
a: z.string(),
s: z.string(),
t: z.string(),
});
export class Cloudflare {
private baseUrl = "https://api.cloudflare.com/client/v4";
@@ -157,14 +164,12 @@ export class Cloudflare {
tunnelName: string,
): Promise<void> {
log.info(`Creating local config for tunnel ${subdomain}.${domain}...`);
const tokenData = JSON.parse(
Buffer.from(tunnelToken, "base64").toString("utf8"),
const tokenData = CloudflareTunnelConfigSchema.parse(
JSON.parse(Buffer.from(tunnelToken, "base64").toString("utf8")),
);
const credentials = {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
AccountTag: tokenData.a || this.accountId,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
TunnelID: tokenData.t || tunnelId,
TunnelName: tunnelName,
TunnelSecret: tokenData.s,
+10 -4
View File
@@ -5,6 +5,10 @@ import http from "http";
import path from "path";
import { fileURLToPath } from "url";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import {
ApiEnvResponse,
ApiPublicLobbiesResponse,
} from "../core/ExpressSchemas";
import { GameInfo, ID } from "../core/Schemas";
import { generateID } from "../core/Util";
import { gatekeeper, LimiterType } from "./Gatekeeper";
@@ -59,7 +63,9 @@ app.use(
}),
);
let publicLobbiesJsonStr = "";
let publicLobbiesJsonStr = JSON.stringify({
lobbies: [],
} satisfies ApiPublicLobbiesResponse);
const publicLobbyIDs: Set<string> = new Set();
@@ -145,8 +151,8 @@ export async function startMaster() {
app.get(
"/api/env",
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
const envConfig = {
game_env: process.env.GAME_ENV,
const envConfig: ApiEnvResponse = {
game_env: process.env.GAME_ENV ?? "",
};
if (!envConfig.game_env) return res.sendStatus(500);
res.json(envConfig);
@@ -266,7 +272,7 @@ async function fetchLobbies(): Promise<number> {
// Update the JSON string
publicLobbiesJsonStr = JSON.stringify({
lobbies: lobbyInfos,
});
} satisfies ApiPublicLobbiesResponse);
return publicLobbyIDs.size;
}
+6 -2
View File
@@ -10,7 +10,11 @@ import { GameEnv } from "../core/configuration/Config";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
import { GameRecord, GameRecordSchema, ID } from "../core/Schemas";
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
import {
CreateGameInputSchema,
GameInputSchema,
WorkerApiGameIdExists,
} from "../core/WorkerSchemas";
import { archive, readGameRecord } from "./Archive";
import { GameManager } from "./GameManager";
import { gatekeeper, LimiterType } from "./Gatekeeper";
@@ -201,7 +205,7 @@ export async function startWorker() {
const lobbyId = req.params.id;
res.json({
exists: gm.game(lobbyId) !== null,
});
} satisfies WorkerApiGameIdExists);
}),
);
+1 -3
View File
@@ -147,9 +147,7 @@ describe("AutoUpgrade Feature", () => {
const event = new AutoUpgradeEvent(100, 200);
const eventString = JSON.stringify(event);
const parsedEvent = JSON.parse(eventString);
expect(parsedEvent.x).toBe(100);
expect(parsedEvent.y).toBe(200);
expect(parsedEvent).toStrictEqual({ x: 100, y: 200 });
});
});
});
+10 -1
View File
@@ -1,5 +1,6 @@
import fs from "fs";
import path from "path";
import { z } from "zod";
describe("LangCode Filename Check", () => {
const langDir = path.join(__dirname, "../resources/lang");
@@ -14,9 +15,17 @@ describe("LangCode Filename Check", () => {
return;
}
const schema = z.object({
lang: z.object({
lang_code: z.string(),
}),
});
for (const file of files) {
const filePath = path.join(langDir, file);
const jsonData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
const jsonData = schema.parse(
JSON.parse(fs.readFileSync(filePath, "utf-8")),
);
const fileNameWithoutExt = path.basename(file, ".json");
const langCode = jsonData.lang?.lang_code;
+8 -1
View File
@@ -21,7 +21,14 @@ describe("Lang SVG Field and File Existence Check", () => {
try {
const filePath = path.join(langDir, file);
const jsonData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
const langSvg = jsonData.lang?.svg;
const langSvg =
jsonData &&
typeof jsonData === "object" &&
"lang" in jsonData &&
jsonData.lang &&
typeof jsonData.lang === "object" &&
"svg" in jsonData.lang &&
jsonData.lang.svg;
if (typeof langSvg !== "string" || langSvg.length === 0) {
errors.push(
`[${file}]: lang.svg is missing or not a non-empty string`,
+1
View File
@@ -9,5 +9,6 @@
"num_land_tiles": 10000,
"width": 100
},
"nations": [],
"name": "Big Plains"
}
@@ -9,5 +9,6 @@
"num_land_tiles": 48,
"width": 8
},
"nations": [],
"name": "Half Land Half Ocean"
}
+1
View File
@@ -9,5 +9,6 @@
"num_land_tiles": 50,
"width": 8
},
"nations": [],
"name": "Ocean and Land"
}
+1
View File
@@ -9,5 +9,6 @@
"num_land_tiles": 2500,
"width": 50
},
"nations": [],
"name": "Plains"
}
+10 -4
View File
@@ -1,5 +1,6 @@
import fs from "fs";
import path from "path";
import { z } from "zod";
import {
Difficulty,
Game,
@@ -12,7 +13,7 @@ import {
import { createGame } from "../../src/core/game/GameImpl";
import {
genTerrainFromBin,
MapManifest,
MapManifestSchema,
} from "../../src/core/game/TerrainMapLoader";
import { UserSettings } from "../../src/core/game/UserSettings";
import { GameConfig } from "../../src/core/Schemas";
@@ -44,9 +45,14 @@ export async function setup(
const mapBinBuffer = fs.readFileSync(mapBinPath);
const miniMapBinBuffer = fs.readFileSync(miniMapBinPath);
const manifest = JSON.parse(
fs.readFileSync(manifestPath, "utf8"),
) satisfies MapManifest;
const str = fs.readFileSync(manifestPath, "utf8");
const raw = JSON.parse(str);
const parsed = MapManifestSchema.safeParse(raw);
if (!parsed.success) {
const error = z.prettifyError(parsed.error);
throw new Error(`Error parsing ${manifestPath}: ${error}`);
}
const manifest = parsed.data;
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
const miniGameMap = await genTerrainFromBin(