Skip to content

[Feature] Generic lazyImportModule helper (DRY ESM default-fallback dance) #252

@pathosDev

Description

@pathosDev

Size / Priority

  • Size: Trivial (~30+ call-sites consolidated)
  • Category: C.2 Simplifications & DRY.
  • Risk: low.

Affected files

  • ~30 sites across the codebase that lazy-import optional peer-deps (memjs, kafkajs, mqtt, nats, amqplib, cassandra-driver, ioredis, etc.).

Background

Optional peer-deps are imported via dynamic import() with the ESM default-export-vs-named-export dance:

const moduleName = 'memjs';
const mod = await import(moduleName);
const client = (mod.default ?? mod).Client.create(...);

The default ?? mod fallback handles modules that ship as either module.exports = { Client } (CommonJS, no default) or module.exports.default = ... (ESM with default export). This appears ~30 times across the broker, journal, cache, and HTTP backend files.

Bugs:

  • Inconsistent fallback shapes (some sites use mod.default ?? mod, others use (mod as any).default || mod, others spread).
  • Error messages on missing peer-dep are bespoke (some good, some terse).
  • TypeScript inference is awkward.

Target code

// src/util/LazyImport.ts (new)

/**
 * Lazy-import an optional peer-dep with a uniform error message.
 *
 *   const memjs = await lazyImportModule<typeof MemjsType>('memjs');
 *   const client = memjs.Client.create(servers);
 */
export async function lazyImportModule<T>(moduleName: string, hint?: string): Promise<T> {
  try {
    const mod = await import(moduleName);
    // ESM default-export-vs-named normalisation
    return ('default' in mod && typeof mod.default !== 'undefined' ? mod.default : mod) as T;
  } catch (e) {
    const installHint = hint ?? `Install the package: bun add ${moduleName}`;
    throw new Error(
      `[actor-ts] optional peer-dep '${moduleName}' is not installed. ${installHint}\n` +
      `Original import error: ${(e as Error).message}`,
    );
  }
}

Per-site usage:

// Before:
const mod = await import('memjs');
const Client = (mod.default ?? mod).Client;

// After:
const memjs = await lazyImportModule<{ Client: typeof MemjsClient }>('memjs');
const Client = memjs.Client;

Integration / risk

  • Behaviour preserved — same import semantics.
  • Error message uniformity — users get the same helpful text everywhere.
  • TypeScript types: caller supplies T; if the user types it correctly, ergonomics improve.

Test plan

  1. Per-site regression — each migrated site loads its peer-dep successfully (verified by existing peer-dep integration tests).
  2. Missing peer-dep: uninstall a peer-dep; the error message includes the install command + module name.
  3. Type-test: TypeScript type returned matches caller expectation.

Acceptance criteria

  • lazyImportModule<T>(name, hint?) exported.
  • All ~30 sites migrated.
  • Per-site tests still pass with peer-dep installed.
  • Missing-peer-dep error message is helpful.
  • No CHANGELOG entry needed.

Pre-implementation note

Some sites do non-trivial transformation after import (e.g., wrap the imported class in a TS-friendly façade). Those sites should keep the transformation but use lazyImportModule for the import + default-resolution step.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: lowNice-to-have / niche / demand-driven

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions