Skip to content

Commit e4185b6

Browse files
Nigel Tatschnerntatschner
authored andcommitted
fix(kb): split reference.ts into client-safe types + server-only fetchers
Build was failing on next-build with: You're importing a component that needs "server-only". Import trace: event-summary-react.tsx → reference.ts → api.ts (server-only) event-summary-react.tsx is a 'use client' file but only needed types + prettyClass + EMPTY_* constants from reference.ts. The fetchers in reference.ts pull api.ts (which is server-only since it carries the Bearer token), and that taints the client bundle. Extracted the client-safe surface into lib/reference-types.ts: - All types (Summary union, ReferenceEntry, ReferenceCatalog, LocationCatalog, EntityDetailOutcome, ReferenceListResponse, …) - All EMPTY_* constants - emptySummary + prettyClass (both pure, no I/O) lib/reference.ts is now marked 'server-only', keeps the fetchers (getCategoryBundle / getEntityDetail / loadAllReferenceBundles / getLocationCatalog), and re-exports everything from reference-types so server callers don't need a new import path. Updated the three client components to import from reference-types: event-summary-react.tsx, EntityLink.tsx, EntityHoverCard.tsx. Verified locally with `pnpm --filter web build` — all three /kb routes appear in the output, no server-only taint.
1 parent 6e2554e commit e4185b6

5 files changed

Lines changed: 320 additions & 274 deletions

File tree

apps/web/src/components/kb/EntityHoverCard.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
* sides together when the field set changes.
1616
*/
1717

18-
import type { ReferenceCategory, ReferenceEntry } from '@/lib/reference';
18+
import type {
19+
ReferenceCategory,
20+
ReferenceEntry,
21+
} from '@/lib/reference-types';
1922

2023
interface EntityHoverCardProps {
2124
category: ReferenceCategory;

apps/web/src/components/kb/EntityLink.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import Link from 'next/link';
2323
import { useState } from 'react';
2424
import type { Route } from 'next';
25-
import type { ReferenceCatalog, ReferenceCategory } from '@/lib/reference';
25+
import type {
26+
ReferenceCatalog,
27+
ReferenceCategory,
28+
} from '@/lib/reference-types';
2629
import { toFriendlyName } from '@/lib/heuristic-name';
2730
import { EntityHoverCard } from './EntityHoverCard';
2831

apps/web/src/lib/event-summary-react.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222
*/
2323

2424
import type { ReactNode } from 'react';
25-
import type { ReferenceCatalogs, ReferenceLookup } from './reference';
26-
import { EMPTY_REFERENCE_CATALOGS, EMPTY_REFERENCE_LOOKUP } from './reference';
25+
import type { ReferenceCatalogs, ReferenceLookup } from './reference-types';
26+
import {
27+
EMPTY_REFERENCE_CATALOGS,
28+
EMPTY_REFERENCE_LOOKUP,
29+
prettyClass,
30+
} from './reference-types';
2731
import { toFriendlyName } from './heuristic-name';
2832
import { EntityLink } from '@/components/kb/EntityLink';
29-
import { prettyClass } from './reference';
3033

3134
// Re-export the payload union from event-summary so consumers don't
3235
// need to import from two places. Importing the type only would
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* Client-safe types + pure helpers for the reference catalogue.
3+
*
4+
* Split out of `./reference` so client components (use-client files
5+
* like `EntityLink`, `EntityHoverCard`, `event-summary-react`) can
6+
* import the type surface without dragging in the server-only
7+
* fetchers — `./reference` imports `apiBase` from `./api`, which is
8+
* marked `import 'server-only'` for token-handling safety. Without
9+
* the split, importing any type from `./reference` taints the
10+
* client bundle and `next build` refuses to compile.
11+
*
12+
* The server-side wrapper `./reference` re-exports every symbol
13+
* from this module, so server callers keep working unchanged.
14+
*/
15+
16+
import { toFriendlyName } from './heuristic-name';
17+
18+
export type ReferenceCategory = 'vehicle' | 'weapon' | 'item' | 'location';
19+
20+
export const CATEGORIES: ReadonlyArray<ReferenceCategory> = [
21+
'vehicle',
22+
'weapon',
23+
'item',
24+
'location',
25+
];
26+
27+
/** Per-category curated summary, internally tagged by `category`
28+
* for discriminated narrowing. Mirrors the Rust `Summary` enum in
29+
* `crates/starstats-server/src/reference_data.rs`. Each variant's
30+
* fields are optional — empty / missing fields are stripped on
31+
* the wire (`skip_serializing_if = "Option::is_none"`). */
32+
export type Summary =
33+
| VehicleSummary
34+
| WeaponSummary
35+
| ItemSummary
36+
| LocationSummary;
37+
38+
export interface VehicleSummary {
39+
category: 'vehicle';
40+
manufacturer?: string;
41+
role?: string;
42+
hull_size?: string;
43+
focus?: string;
44+
}
45+
46+
export interface WeaponSummary {
47+
category: 'weapon';
48+
manufacturer?: string;
49+
size?: string;
50+
damage_type?: string;
51+
weapon_type?: string;
52+
}
53+
54+
export interface ItemSummary {
55+
category: 'item';
56+
manufacturer?: string;
57+
item_type?: string;
58+
grade?: string;
59+
}
60+
61+
export interface LocationSummary {
62+
category: 'location';
63+
system?: string;
64+
parent?: string;
65+
tag?: string;
66+
classification?: string;
67+
}
68+
69+
/** Empty/default summary for a given category — used when the
70+
* server returns nothing (or hasn't synced yet). Lets consumers
71+
* rely on `summary.category` being present even on a freshly-
72+
* defaulted entry. */
73+
export function emptySummary(category: ReferenceCategory): Summary {
74+
switch (category) {
75+
case 'vehicle':
76+
return { category: 'vehicle' };
77+
case 'weapon':
78+
return { category: 'weapon' };
79+
case 'item':
80+
return { category: 'item' };
81+
case 'location':
82+
return { category: 'location' };
83+
}
84+
}
85+
86+
/** Slim per-entry shape returned by `/v1/reference/{category}`. */
87+
export interface ReferenceEntry {
88+
category: ReferenceCategory;
89+
class_name: string;
90+
display_name: string;
91+
/** URL-safe canonical identifier. Null on legacy rows pre-dating
92+
* the KB-v1 backfill — callers should fall back to a
93+
* `class_name` URL when null. */
94+
slug?: string | null;
95+
/** Per-category curated fields as a discriminated union. */
96+
summary: Summary;
97+
}
98+
99+
/** Detail shape returned by per-entry endpoints, with the full
100+
* `metadata` blob retained for the detail page. The listing
101+
* endpoint never returns this shape — only the slim
102+
* `ReferenceEntry`. */
103+
export interface ReferenceEntryDetail extends ReferenceEntry {
104+
metadata: Record<string, unknown>;
105+
}
106+
107+
/** Map keyed by lowercased class_name → display_name. Retained for
108+
* the legacy `prettyClass` callers in `event-summary.ts` and the
109+
* journey/dashboard pages; new code should prefer
110+
* [`ReferenceCatalog`] which keeps slug + summary attached. */
111+
export type ReferenceMap = ReadonlyMap<string, string>;
112+
113+
/** Rich map keyed by lowercased class_name → full slim entry. */
114+
export type ReferenceCatalog = ReadonlyMap<string, ReferenceEntry>;
115+
116+
/** One Map per category. Each Map is empty (not absent) on fetch
117+
* failure so callers don't need a per-category presence check. */
118+
export interface ReferenceLookup {
119+
vehicles: ReferenceMap;
120+
weapons: ReferenceMap;
121+
items: ReferenceMap;
122+
locations: ReferenceMap;
123+
}
124+
125+
/** Empty lookup — safe default for callers before the fetch
126+
* resolves. */
127+
export const EMPTY_REFERENCE_LOOKUP: ReferenceLookup = {
128+
vehicles: new Map(),
129+
weapons: new Map(),
130+
items: new Map(),
131+
locations: new Map(),
132+
};
133+
134+
/** Catalog form keyed by class_name → full slim entry, one per
135+
* category. Populated alongside `ReferenceLookup` from the same
136+
* fetch, so passing both around stays cheap. */
137+
export interface ReferenceCatalogs {
138+
vehicles: ReferenceCatalog;
139+
weapons: ReferenceCatalog;
140+
items: ReferenceCatalog;
141+
locations: ReferenceCatalog;
142+
}
143+
144+
/** Empty catalog set — safe default before the fetch resolves. */
145+
export const EMPTY_REFERENCE_CATALOGS: ReferenceCatalogs = {
146+
vehicles: new Map(),
147+
weapons: new Map(),
148+
items: new Map(),
149+
locations: new Map(),
150+
};
151+
152+
/** Bundle of both views produced by a single category fetch. */
153+
export interface CategoryBundle {
154+
map: ReferenceMap;
155+
catalog: ReferenceCatalog;
156+
}
157+
158+
export const EMPTY_CATEGORY_BUNDLE: CategoryBundle = {
159+
map: new Map(),
160+
catalog: new Map(),
161+
};
162+
163+
/** Outcome of a `getEntityDetail` call. `not_found` lets the
164+
* detail page render the dedicated 404 path; `error` lets the
165+
* page distinguish transient backend trouble from a genuinely
166+
* missing entity (the old single-`null` collapse rendered a
167+
* permanent "not found" on a transient 503, which is misleading). */
168+
export type EntityDetailOutcome =
169+
| { kind: 'ok'; entry: ReferenceEntryDetail }
170+
| { kind: 'not_found' }
171+
| { kind: 'error'; reason: string };
172+
173+
/**
174+
* Resolve a raw class identifier through a category Map; on miss,
175+
* fall through to the heuristic prettifier so the UI never renders
176+
* a bare underscored identifier. Pure — no fetches, no I/O.
177+
*/
178+
export function prettyClass(
179+
raw: string | null | undefined,
180+
map: ReferenceMap,
181+
): string {
182+
if (!raw) return '';
183+
return map.get(raw.toLowerCase()) ?? toFriendlyName(raw);
184+
}
185+
186+
// -- Location catalog types (Wave 1: catalog-driven hierarchy) ---------
187+
188+
/** Trimmed shape of a wiki location entry — only the fields we use
189+
* for hierarchy resolution. Sourced from the raw wiki JSON which
190+
* the server persists verbatim into `reference_registry.metadata`. */
191+
export interface LocationEntry {
192+
/** Engine join key as the server stores it. Usually the wiki
193+
* `slug` (e.g. `aberdeen-2`) since the wiki has no
194+
* `class_name` field for locations. */
195+
classKey: string;
196+
/** Canonical display name (`"Aberdeen"`). */
197+
displayName: string;
198+
/** Parent system display from `star.name` (`"Stanton"`). */
199+
system: string | null;
200+
/** Parent body from `parent.name`. Null when the entry IS a
201+
* planet or has no parent. */
202+
parent: string | null;
203+
/** Engine-internal joined short form from `tag.name`
204+
* (`"Stanton1b"`). Primary match candidate against event
205+
* payloads. */
206+
tag: string | null;
207+
/** URL slug (`"aberdeen-2"`). Match fallback. */
208+
slug: string | null;
209+
/** `type.classification` — `"Star"` / `"Planet"` / `"Moon"` /
210+
* `"City"` / `"Station"` / `"Outpost"`. Drives display
211+
* decisions (e.g. don't render `parent` for a planet). */
212+
classification: string | null;
213+
}
214+
215+
/** Multi-index lookup over the location catalog. Several keys per
216+
* entry so the parser can match by name, by engine tag, or by slug
217+
* without knowing which form an event payload uses. */
218+
export interface LocationCatalog {
219+
byName: ReadonlyMap<string, LocationEntry>;
220+
byTag: ReadonlyMap<string, LocationEntry>;
221+
bySlug: ReadonlyMap<string, LocationEntry>;
222+
display: ReferenceMap;
223+
/** Lookup of `class_name (lowercased) → slug`. Used by the
224+
* HierarchicalBucketList to turn aggregate leaves into
225+
* `/kb/location/{slug}` links when the leaf represents exactly
226+
* one wiki-known location. Empty when slug backfill hasn't run. */
227+
slugByClass: ReadonlyMap<string, string>;
228+
}
229+
230+
/** Empty catalog — safe default when the fetch fails or hasn't
231+
* resolved yet. */
232+
export const EMPTY_LOCATION_CATALOG: LocationCatalog = {
233+
byName: new Map(),
234+
byTag: new Map(),
235+
bySlug: new Map(),
236+
display: new Map(),
237+
slugByClass: new Map(),
238+
};
239+
240+
/** Internal: listing-endpoint response shape used by the fetchers
241+
* in `./reference`. Exported here so the server-side wrapper can
242+
* type its JSON deserialisations against the same union the rest
243+
* of the codebase reads. */
244+
export interface ReferenceListResponse {
245+
entries: Array<{
246+
class_name: string;
247+
display_name: string;
248+
slug?: string | null;
249+
summary?: Summary;
250+
}>;
251+
}

0 commit comments

Comments
 (0)