ruvnet/ruflo · error · Error

Module ${modulePath} does not export a plugin

Error message

Module ${modulePath} does not export a plugin

What it means

loadPlugin(modulePath) dynamically imports the module and expects `export default` or a named `plugin` export containing an IPlugin instance or an async factory function. If neither export exists it throws before touching the registry. The lookup is exactly `module.default ?? module.plugin`, so any other export shape fails.

Source

Thrown at v3/@claude-flow/plugins/src/index.ts:342

}

/**
 * Load and register a plugin module dynamically.
 *
 * @example
 * ```typescript
 * const plugin = await loadPlugin('./my-plugin.js');
 * ```
 */
export async function loadPlugin(
  modulePath: string,
  config?: Partial<PluginConfig>
): Promise<IPlugin> {
  const module = await import(modulePath);
  const pluginOrFactory: IPlugin | PluginFactory = module.default ?? module.plugin;

  if (!pluginOrFactory) {
    throw new Error(`Module ${modulePath} does not export a plugin`);
  }

  const plugin = typeof pluginOrFactory === 'function'
    ? await pluginOrFactory()
    : pluginOrFactory;

  await getDefaultRegistry().register(plugin, config);
  return plugin;
}

/**
 * Initialize the plugin system.
 *
 * @example
 * ```typescript
 * await initializePlugins({
 *   coreVersion: '3.0.0',
 *   dataDir: './data',

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Ensure the target module has `export default plugin` (or `export const plugin = ...`) — instance or async factory both work
  2. Debug the resolution: `const m = await import(path); console.log(Object.keys(m), m.default)` and fix the path or export accordingly
  3. For CJS plugins use `module.exports = plugin` so interop exposes it as default, or re-export via a small ESM wrapper

Example fix

// before (my-plugin/index.js)
export class MyPlugin { /* ... */ } // no default, no `plugin` export

// after
export class MyPlugin { /* ... */ }
export default new MyPlugin();
// or: export const plugin = async () => new MyPlugin();
Defensive patterns

Strategy: type-guard

Validate before calling

// Inspect the module before handing it to loadPlugin:
const mod: any = await import(modulePath);
if (!mod?.default && !mod?.plugin) {
  throw new Error(
    `${modulePath} exports [${Object.keys(mod).join(', ')}] — need default or 'plugin'`
  );
}

Type guard

type PluginExport = { default?: unknown; plugin?: unknown };
function isPluginExport(mod: PluginExport): boolean {
  const candidate = mod.default ?? mod.plugin;
  return typeof candidate === 'object' || typeof candidate === 'function';
}

Try / catch

try {
  plugin = await loadPlugin(modulePath, config);
} catch (err) {
  if (err instanceof Error && /does not export a plugin/.test(err.message)) {
    // log Object.keys(await import(modulePath)) to see the real export shape
  }
  throw err;
}

Prevention

When it happens

Trigger: The plugin module exports only a named class or const other than `plugin`; a CommonJS module.exports without ESM interop so module.default is undefined; the path resolving to the wrong file (typo, missing extension in native ESM, barrel index re-export that drops the plugin); a package whose main entry is not the plugin module.

Common situations: Third-party plugin packages using non-standard export names; CJS/ESM interop mismatches under tsconfig `esModuleInterop` changes; path mistakes under bundler-less Node ESM where extensions matter; refactors that moved the default export into a named one.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/76c04230837aaf7c. Report an issue: GitHub.