component-loader.ts
PURPOSE
Engine-layer loader for every VFX component definition. Resolves all data/vfx/components/*.component.json files at build/test time via Vite’s import.meta.glob and exposes them as a sorted array, an id-keyed map, and a strict lookup helper. Single source-of-truth used by both the VFX Workbench Pane (authoring UI) and any test helpers — no separate fixture path.
OWNS
ALL_COMPONENTS: readonly ComponentDef[]— every loadedComponentDef, sorted byid.localeCompare. Frozen at module-load time.COMPONENT_MAP: ReadonlyMap<string, ComponentDef>—id → ComponentDef, built once fromALL_COMPONENTS.loadAllComponents()— returnsALL_COMPONENTScast to a mutableComponentDef[](callers shouldn’t mutate, but the cast exists for ergonomics).getComponent(id)— strict lookup that throwsError("component '<id>' not found. Available: <sorted ids>")on miss.
READS FROM
../../data/vfx/components/*.component.json— every file matching this glob, eagerly imported as JSON via Vite’simport.meta.globwith{ eager: true, import: 'default' }../component-schema—ComponentDeftype only (type-only import).
PUSHES TO
- No runtime side effects. Module-load builds the array + map once; no events, no stores, no I/O after evaluation.
- Consumers (workbench Pane, test helpers) pull from the exported bindings.
DOES NOT
- Validate JSON shape against the schema at runtime — JSON is trusted to match
ComponentDef. Any schema validation lives elsewhere (component-schema/ tests). - Watch for file changes at runtime — the glob is resolved at build time. Adding a new
.component.jsonrequires a rebuild / Vite HMR. - Lazy-load —
eager: truemeans every component JSON is bundled and parsed on first import of this module. - Defend against duplicate
ids — last-write-wins in theMap, no error. The sortedALL_COMPONENTSwill still contain both entries. - Cache anything cross-session — module state only.
Signals
None. Pure data exposure.
Entry points
loadAllComponents()— called by VFX Workbench Pane to populate the component picker.getComponent(id)— strict accessor for code paths that need a specific component (effect graph resolution, tests).ALL_COMPONENTS/COMPONENT_MAP— direct binding access for iteration / lookup without function call overhead.
Pattern notes
- Same
import.meta.globpattern asfixtures.ts, deliberately lifted to the engine layer so the authoring Pane and test helpers share one path. Comment at top of file documents this intent. - Sort is by
id.localeCompare(stable, locale-aware lexicographic) — UI gets a deterministic order regardless of filesystem enumeration. getComponenterror message embeds the sorted list of available ids — turns “component not found” into an actionable diagnostic at throw time (cheap because library is small).- Readonly-typed exports (
readonly ComponentDef[],ReadonlyMap) signal intent;loadAllComponentscasts awayreadonlyfor legacy callers that expect a mutable array.
EXTRACT-CANDIDATE
The import.meta.glob → sorted array + id-keyed Map + strict getter pattern is duplicated across fixtures.ts and likely other data-table loaders. A generic createIdMapLoader<T extends { id: string }>(glob) factory would deduplicate three near-identical loaders. Not urgent — three call sites is the threshold per CLAUDE.md, and the bodies are short.