build: migrate build system to Vite and test runner to Vitest & Remove depracated husky usage (#2703)

- Replace Webpack with Vite for faster client bundling and HMR.
- Migrate tests from Jest to Vitest and update configuration.
- Update Web Worker instantiation to standard ESM syntax.
- Implement Env utility in `src/core` for safe, hybrid environment
variable access (Vite vs Node).
- Refactor configuration loaders to remove direct `process.env`
dependencies in shared code.
- Update TypeScript environment definitions and project scripts for the
new toolchain.
- Remove the [depracated usage of the
husky](https://github.com/typicode/husky/releases/tag/v9.0.1).

## Description:

migrate build system to Vite and test runner to Vitest & Remove
depracated husky usage

## 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
- [ ] 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

## Please put your Discord username so you can be contacted if a bug or
regression is found:

wraith4081

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Wraith
2025-12-28 22:10:26 -08:00
committed by GitHub
co-authored by evanpelle
parent f6412a5979
commit 26f5d40819
75 changed files with 2765 additions and 10503 deletions
+4 -7
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { ProgressBar } from "../../../src/client/graphics/ProgressBar";
describe("ProgressBar", () => {
@@ -15,9 +12,9 @@ describe("ProgressBar", () => {
});
it("should initialize and draw the background", () => {
const spyClearRect = jest.spyOn(ctx, "clearRect");
const spyFillRect = jest.spyOn(ctx, "fillRect");
const spyFillStyle = jest.spyOn(ctx, "fillStyle", "set");
const spyClearRect = vi.spyOn(ctx, "clearRect");
const spyFillRect = vi.spyOn(ctx, "fillRect");
const spyFillStyle = vi.spyOn(ctx, "fillStyle", "set");
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10, 0.5);
expect(spyClearRect).toHaveBeenCalledWith(0, 0, 82, 12);
expect(spyFillRect).toHaveBeenCalledWith(1, 1, 80, 10);
@@ -28,7 +25,7 @@ describe("ProgressBar", () => {
it("should set progress and draw the progress bar", () => {
const bar = new ProgressBar(["#ff0000", "#00ff00"], ctx, 2, 2, 80, 10);
const spyFillRect = jest.spyOn(ctx, "fillRect");
const spyFillRect = vi.spyOn(ctx, "fillRect");
bar.setProgress(0.5);
expect(bar.getProgress()).toBe(0.5);
expect(spyFillRect).toHaveBeenCalledWith(
@@ -1,6 +1,4 @@
/**
* @jest-environment jsdom
*/
import { vi, type Mock } from "vitest";
import {
attackMenuElement,
buildMenuElement,
@@ -13,13 +11,15 @@ import { UnitType } from "../../../src/core/game/Game";
import { TileRef } from "../../../src/core/game/GameMap";
import { GameView, PlayerView } from "../../../src/core/game/GameView";
jest.mock("../../../src/client/Utils", () => ({
translateText: jest.fn((key: string) => key),
renderNumber: jest.fn((num: number) => num.toString()),
vi.mock("../../../src/client/Utils", () => ({
translateText: vi.fn((key: string) => key),
renderNumber: vi.fn((num: number) => num.toString()),
}));
jest.mock("../../../src/client/graphics/layers/BuildMenu", () => {
const { UnitType } = jest.requireActual("../../../src/core/game/Game");
vi.mock("../../../src/client/graphics/layers/BuildMenu", async () => {
const { UnitType } = await vi.importActual<
typeof import("../../../src/core/game/Game")
>("../../../src/core/game/Game");
return {
flattenedBuildTable: [
{
@@ -68,14 +68,14 @@ jest.mock("../../../src/client/graphics/layers/BuildMenu", () => {
};
});
jest.mock("nanoid", () => ({
customAlphabet: jest.fn(() => jest.fn(() => "mock-id")),
vi.mock("nanoid", () => ({
customAlphabet: vi.fn(() => vi.fn(() => "mock-id")),
}));
jest.mock("dompurify", () => ({
vi.mock("dompurify", () => ({
__esModule: true,
default: {
sanitize: jest.fn((str: string) => str),
sanitize: vi.fn((str: string) => str),
},
}));
@@ -90,29 +90,29 @@ describe("RadialMenuElements", () => {
beforeEach(() => {
mockPlayer = {
id: () => 1,
isAlliedWith: jest.fn(() => false),
isPlayer: jest.fn(() => true),
isAlliedWith: vi.fn(() => false),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockGame = {
inSpawnPhase: jest.fn(() => false),
owner: jest.fn(() => mockPlayer),
isLand: jest.fn(() => true),
config: jest.fn(() => ({
inSpawnPhase: vi.fn(() => false),
owner: vi.fn(() => mockPlayer),
isLand: vi.fn(() => true),
config: vi.fn(() => ({
theme: () => ({
territoryColor: () => ({
lighten: () => ({ alpha: () => ({ toRgbString: () => "#fff" }) }),
}),
}),
isUnitDisabled: jest.fn(() => false),
isUnitDisabled: vi.fn(() => false),
})),
} as unknown as GameView;
mockBuildMenu = {
canBuildOrUpgrade: jest.fn(() => true),
cost: jest.fn(() => 100),
count: jest.fn(() => 5),
sendBuildOrUpgrade: jest.fn(),
canBuildOrUpgrade: vi.fn(() => true),
cost: vi.fn(() => 100),
count: vi.fn(() => 5),
sendBuildOrUpgrade: vi.fn(),
};
mockPlayerActions = {
@@ -148,7 +148,7 @@ describe("RadialMenuElements", () => {
playerPanel: {} as any,
chatIntegration: {} as any,
eventBus: {} as any,
closeMenu: jest.fn(),
closeMenu: vi.fn(),
};
});
@@ -161,19 +161,19 @@ describe("RadialMenuElements", () => {
});
it("should be disabled during spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => true);
mockGame.inSpawnPhase = vi.fn(() => true);
expect(attackMenuElement.disabled(mockParams)).toBe(true);
});
it("should be enabled when not in spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => false);
mockGame.inSpawnPhase = vi.fn(() => false);
expect(attackMenuElement.disabled(mockParams)).toBe(false);
});
it("should return attack submenu with attack units only", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -203,7 +203,7 @@ describe("RadialMenuElements", () => {
it("should not include construction units in attack menu", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -237,12 +237,12 @@ describe("RadialMenuElements", () => {
});
it("should be disabled during spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => true);
mockGame.inSpawnPhase = vi.fn(() => true);
expect(buildMenuElement.disabled(mockParams)).toBe(true);
});
it("should be enabled when not in spawn phase", () => {
mockGame.inSpawnPhase = jest.fn(() => false);
mockGame.inSpawnPhase = vi.fn(() => false);
expect(buildMenuElement.disabled(mockParams)).toBe(false);
});
@@ -313,9 +313,9 @@ describe("RadialMenuElements", () => {
it("should show attack and boat menu on enemy territory", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockGame.owner = jest.fn(() => enemyPlayer);
mockGame.owner = vi.fn(() => enemyPlayer);
const subMenu = rootMenuElement.subMenu!(mockParams);
const buildMenu = subMenu.find((item) => item.id === Slot.Build);
@@ -337,8 +337,8 @@ describe("RadialMenuElements", () => {
it("should handle ally menu correctly", () => {
const allyPlayer = {
id: () => 2,
isAlliedWith: jest.fn(() => true),
isPlayer: jest.fn(() => true),
isAlliedWith: vi.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = allyPlayer;
@@ -367,7 +367,7 @@ describe("RadialMenuElements", () => {
it("should execute attack action correctly", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -389,7 +389,7 @@ describe("RadialMenuElements", () => {
it("should not execute action when buildable unit is not found", () => {
mockPlayerActions.buildableUnits = [];
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false);
mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false);
const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City");
@@ -420,7 +420,7 @@ describe("RadialMenuElements", () => {
it("should generate correct tooltip items for attack elements", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -452,7 +452,7 @@ describe("RadialMenuElements", () => {
it("should use correct colors for attack elements", () => {
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
@@ -465,7 +465,7 @@ describe("RadialMenuElements", () => {
});
it("should not set color when element is disabled", () => {
mockBuildMenu.canBuildOrUpgrade = jest.fn(() => false);
mockBuildMenu.canBuildOrUpgrade = vi.fn(() => false);
const subMenu = buildMenuElement.subMenu!(mockParams);
const cityElement = subMenu.find((item) => item.id === "build_City");
@@ -475,10 +475,12 @@ describe("RadialMenuElements", () => {
});
describe("Translation integration", () => {
it("should use translateText for tooltip items in build menu", () => {
const { translateText } = jest.requireMock("../../../src/client/Utils");
it("should use translateText for tooltip items in build menu", async () => {
const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
(translateText as jest.Mock).mockClear();
(translateText as Mock).mockClear();
buildMenuElement.subMenu!(mockParams);
@@ -488,14 +490,16 @@ describe("RadialMenuElements", () => {
expect(translateText).toHaveBeenCalledWith("unit_type.factory_desc");
});
it("should use translateText for tooltip items in attack menu", () => {
const { translateText } = jest.requireMock("../../../src/client/Utils");
it("should use translateText for tooltip items in attack menu", async () => {
const { translateText } = await vi.importMock<
typeof import("../../../src/client/Utils")
>("../../../src/client/Utils");
(translateText as jest.Mock).mockClear();
(translateText as Mock).mockClear();
const enemyPlayer = {
id: () => 2,
isPlayer: jest.fn(() => true),
isPlayer: vi.fn(() => true),
} as unknown as PlayerView;
mockParams.selected = enemyPlayer;
+2 -5
View File
@@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { UILayer } from "../../../src/client/graphics/layers/UILayer";
import { UnitSelectionEvent } from "../../../src/client/InputHandler";
import { UnitView } from "../../../src/core/game/GameView";
@@ -28,7 +25,7 @@ describe("UILayer", () => {
ticks: () => 1,
updatesSinceLastTick: () => undefined,
};
eventBus = { on: jest.fn() };
eventBus = { on: vi.fn() };
transformHandler = {};
});
@@ -50,7 +47,7 @@ describe("UILayer", () => {
owner: () => ({}),
};
const event = { isSelected: true, unit };
ui.drawSelectionBox = jest.fn();
ui.drawSelectionBox = vi.fn();
ui["onUnitSelection"](event as UnitSelectionEvent);
expect(ui.drawSelectionBox).toHaveBeenCalledWith(unit);
});
@@ -1,20 +1,20 @@
jest.mock("lit", () => ({
vi.mock("lit", () => ({
html: () => {},
LitElement: class {},
}));
jest.mock("lit/decorators.js", () => ({
vi.mock("lit/decorators.js", () => ({
customElement: () => (clazz: any) => clazz,
query: () => () => {},
state: () => () => {},
property: () => () => {},
}));
jest.mock("lit/directive.js", () => ({
vi.mock("lit/directive.js", () => ({
DirectiveResult: class {},
}));
jest.mock("lit/directives/unsafe-html.js", () => ({
vi.mock("lit/directives/unsafe-html.js", () => ({
unsafeHTML: () => {},
UnsafeHTMLDirective: class {},
}));