halo-dev/halo · error · TypeError

PluginModule.components must be an object.

Error message

PluginModule.components must be an object.

What it means

Thrown by preparePluginModule when a plugin's PluginModule.components field is present but not a plain object (isRecord returns false — i.e. it is null, an array, a primitive, or a function). components must be a Record<string, Component> so Halo can globally register each named component. Activating a plugin whose module has the wrong shape here would register garbage, so the host throws a TypeError during module preparation.

Source

Thrown at ui/src/setup/setupModules.ts:668

      replacedRoute.route,
      replacedRoute.owner,
      routeOwners,
      componentOwners
    );
  }
}

function preparePluginModule(
  module: PluginModule,
  registration: UiProviderRegistration,
  report: (
    registration: UiProviderRegistration,
    stage: UiPluginFailureStage,
    error: unknown
  ) => void
) {
  if (module.components && !isRecord(module.components)) {
    throw new TypeError("PluginModule.components must be an object.");
  }
  if (module.routes && !Array.isArray(module.routes)) {
    throw new TypeError("PluginModule.routes must be an array.");
  }
  if (module.ucRoutes && !Array.isArray(module.ucRoutes)) {
    throw new TypeError("PluginModule.ucRoutes must be an array.");
  }
  if (module.formkit && !isRecord(module.formkit)) {
    throw new TypeError("PluginModule.formkit must be an object.");
  }
  if (module.extensionPoints && !isRecord(module.extensionPoints)) {
    throw new TypeError("PluginModule.extensionPoints must be an object.");
  }
  resetRouteMetaAndWrapChunks(module.routes, false, registration, report);
  resetRouteMetaAndWrapChunks(module.ucRoutes, false, registration, report);
}

function resetRouteMetaAndWrapChunks(

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Return components as an object keyed by component name: { components: { MyWidget } } (using shorthand) or { components: { 'MyWidget': MyWidget } }.
  2. Remove the components field entirely if the plugin registers no global components.
  3. Run `pnpm -C ui typecheck` against the plugin to catch shape errors against the PluginModule type.

Example fix

// before
export default definePluginModule({ components: [MyWidget, Other] });

// after
export default definePluginModule({ components: { MyWidget, Other } });
Defensive patterns

Strategy: type-guard

Validate before calling

function assertComponentsShape(module: { components?: unknown }) {
  if (module.components && (typeof module.components !== "object" || module.components === null || Array.isArray(module.components))) {
    throw new TypeError("PluginModule.components must be an object.");
  }
}

Type guard

function isComponentRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

Prevention

When it happens

Trigger: A plugin's activated module returns { components: [...] } (array) or { components: () => Foo } (function) or { components: null }, processed at activation in preparePluginModule (line 393 call site).

Common situations: A plugin author mistakes components for an array; assigns a single component instead of a map; a build step mis-types the module; copying a snippet that returns an array of components.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/2841d8c458bcadcb. Report an issue: GitHub.