Web client for TLE Community.
app CLAUDE.md
7.2 kB
Markdown
at main

CLAUDE.md #

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project #

Web client for TLE Community (The Lacuna Expanse), a community-run continuation of the Lacuna Expanse game. This repo talks to a separate backend server (tlecommunity/server) over JSON-RPC; it does not run the game logic itself. The project has gone through a couple of names over the years (originally "Lacuna", later "Kantigen"/"KA") — current naming is tlecommunity / "TLE Community", so avoid introducing old "Kantigen", "KA", "ka-web", or "ka-server" naming into new code, comments, or docs.

Commands #

  • npm start — start Vite dev server against a real backend (needs LACUNA_SERVER_URL etc. set, see .env.local)
  • npm run start:stubbed — run the local stubbed server (stubs/server.ts, port 3005) and Vite (--mode stubbed, loading .env.stubbed) together via concurrently. The stub server runs under tsx watch. npm run serve:stubs runs just the stub server.
  • npm run lint:check — Oxlint (config in oxlint.config.ts); npm run lint:fix applies autofixes
  • npm run format:check — Prettier check; npm run format:fix writes in place
  • npm run types:check — tsc --noEmit
  • npm test — Jest; npm run test:watch for watch mode, npm run test:coverage for coverage
    • Single file: npx jest app/path/to/file.test.ts
    • Single test: npx jest -t "test name"
  • npm run build — production build via vite build

Node >=24 and npm >=10 are required (engines in package.json). The @tlecommunity/client dependency is a private scoped package resolved from a self-hosted Gitea npm registry (see .npmrc), not the public npm registry.

Architecture #

The codebase is mid-migration from a legacy YUI2 client to React/TypeScript, so two UI layers currently coexist:

  • app/yui/** — legacy YUI2 + jQuery code, being incrementally eliminated. Oxlint ignores this directory entirely; don't hold it to current lint/style standards.
  • app/components/** — current React/TypeScript UI. New feature work belongs here.

Backend data lives in TanStack Query; MobX holds only what a query cannot own. docs/ (see below) predates both and is inaccurate on this point.

Data fetching belongs in app/queries/* — one file per API module, exporting useQuery/useMutation hooks. Components must not call lacuna.* directly; add a hook instead. The rules:

  • Every query key comes from app/queryKeys.ts. Never inline a key at a call site — a key and the invalidation that busts it have to move together. Keys are hierarchical tuples, so queryKeys.building.all invalidates every building view while queryKeys.building.view(url, id) targets one.
  • Every queryFn/mutationFn wraps its call in unwrap() (app/queries/unwrap.ts). The client never rejects — it resolves to { result, error } — and React Query needs a rejection to see a failure. unwrap throws a LacunaError; the user-facing alert is still the global onResponse handler in app/lacuna.ts.
  • When a write makes a read stale, invalidate it in the mutation's onSuccess. Prefer mutate with callbacks over await mutateAsync(), so a server error cannot escape as an unhandled rejection.
  • app/components/queryState.tsx renders the loading and error branches, so components do not each grow their own spinner.
  • app/queryClient.ts is a module-level singleton, which is how non-React code (the legacy YUI layer) invalidates.

Stores live under app/stores/:

  • app/stores/rpc/* — the server, empire and body "status blocks". These stay MobX on purpose: they are push-based, arriving on any response via splitStatus() (app/server.ts), and are extrapolated every second by app/stores/ticker.ts. No single query owns them. Anything that is the response to one specific call belongs in app/queries/ instead.
  • app/stores/windows.ts + app/stores/window/* — the window manager. WindowsStore holds a z-index-ordered stack of Window objects, one per WindowType (enumerated in app/interfaces/window.ts); adding a window of an already-open type replaces it rather than stacking a duplicate. Rendered by app/components/menu/gameWindow.
  • app/stores/menu.ts, session.ts, ticker.ts — global UI/session/clock state.

Server communication flow: app/lacuna.ts exports the configured @tlecommunity/client singleton — typed endpoints as properties (lacuna.empire.viewBoosts(), lacuna.body.getBuildings({...}), lacuna.buildingFromUrl(url).view({...})), each resolving to { result, error } rather than throwing. It also registers the one global onResponse handler, which routes status blocks into MobX, alerts on errors, and owns the captcha (1016), empire-not-founded (1100) and session-expired flows. It deliberately does not enable the client's enableRpcLimitHandler() auto-retry: that suits unattended scripts, whereas here the user initiated the action and should see the RPC limit (1010) error, wait it out, and decide what to do next. app/server.ts is now just splitStatus. There is no app/services/ layer any more; app/queries/* replaced it.

Other structural pieces:

  • app/interfaces/* — TS types for API params/responses and domain models (building, body, empire, window, tabber).
  • app/hocs/* — React HOCs, e.g. withBuildingData, which runs the building view query and injects the result as a prop. It is the single fetch point behind all six building windows.
  • app/shims/* — do not import $ from 'jquery' directly; import the shim at app/shims/jquery instead (it combines jQuery plugins used by legacy code into one object).
  • Env vars use a LACUNA_ prefix (Vite envPrefix), typed in app/env.d.ts. .env.local/.env.stubbed/.env.production select which backend/assets URLs are used.

docs/ folder #

docs/ exists but its own docs/README.md warns "most of the information in here is rather out of date." It documents a Reflux/Gulp-era architecture (Actions → Stores → Components via Reflux, gulp dev, js/ source paths) that predates the current Vite/MobX/app/ setup. Treat it as historical background only — do not follow docs/guides/implementing-a-building.md or docs/add-buildings.md as literal instructions for adding a building; use an existing building implementation under app/components/building and app/queries as the template instead.

Testing #

  • Jest is configured with clearMocks: true (jest.config.js), so mocks are cleared automatically between tests — do not call jest.clearAllMocks() in beforeEach/afterEach.
  • Prefer Testing Library's userEvent (e.g. userEvent.click()) over fireEvent — it dispatches the fuller sequence of events a real browser/user interaction produces, so it more closely mirrors actual user behavior. Reach for fireEvent only when testing a specific low-level event that userEvent doesn't model.

Code style #

  • Oxlint config defined in oxlint.config.ts
  • Prettier: single quotes, semicolons, trailing commas (es5), 100 print width, 2-space indent.
  • camelCase file names; no index.ts/index.tsx barrel files.