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 loaded ComponentDef, sorted by id.localeCompare. Frozen at module-load time.
  • COMPONENT_MAP: ReadonlyMap<string, ComponentDef>id → ComponentDef, built once from ALL_COMPONENTS.
  • loadAllComponents() — returns ALL_COMPONENTS cast to a mutable ComponentDef[] (callers shouldn’t mutate, but the cast exists for ergonomics).
  • getComponent(id) — strict lookup that throws Error("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’s import.meta.glob with { eager: true, import: 'default' }.
  • ./component-schemaComponentDef type 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.json requires a rebuild / Vite HMR.
  • Lazy-load — eager: true means every component JSON is bundled and parsed on first import of this module.
  • Defend against duplicate ids — last-write-wins in the Map, no error. The sorted ALL_COMPONENTS will 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.glob pattern as fixtures.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.
  • getComponent error 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; loadAllComponents casts away readonly for 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.