halo-dev/halo · error · TypeError

PluginModule.routes must be an array.

Error message

PluginModule.routes must be an array.

What it means

Thrown by preparePluginModule when module.routes is present but not an array. routes must be RouteRecordRaw[] | RouteRecordAppend[] so Halo can iterate and register console routes (with meta/chunk wrapping). A non-array value would break the route registration loop, so the host throws a TypeError before touching the router.

Source

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

      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(
  routes: RouteRecordRaw[] | RouteRecordAppend[] | undefined,
  core: boolean,
  registration?: UiProviderRegistration,

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Wrap the route(s) in an array: { routes: [{ path: '/x', component: X }] }.
  2. Omit routes if the plugin registers no console routes.
  3. Typecheck the plugin module against PluginModule before release.

Example fix

// before
export default definePluginModule({ routes: { name: "foo", path: "/foo", component: Foo } });

// after
export default definePluginModule({ routes: [{ name: "foo", path: "/foo", component: Foo }] });
Defensive patterns

Strategy: type-guard

Validate before calling

function assertRoutesShape(module: { routes?: unknown }) {
  if (module.routes && !Array.isArray(module.routes)) {
    throw new TypeError("PluginModule.routes must be an array.");
  }
}

Type guard

function isRouteArray(value: unknown): value is unknown[] {
  return Array.isArray(value);
}

Prevention

When it happens

Trigger: A plugin module returns { routes: { path: '/x' } } (a single object) or { routes: 'x' } during activation; preparePluginModule reads module.routes and Array.isArray fails.

Common situations: Plugin author returns one route object instead of an array; merges routes from a nested object; a typo assigns a string.

Related errors


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