Skip to content

fix: replace broken esmRequire for internal modules with static ESM imports#28

Merged
morozow merged 1 commit intomainfrom
fix/esm-modules-ai-providers-import-module
Apr 30, 2026
Merged

fix: replace broken esmRequire for internal modules with static ESM imports#28
morozow merged 1 commit intomainfrom
fix/esm-modules-ai-providers-import-module

Conversation

@morozow
Copy link
Copy Markdown
Member

@morozow morozow commented Apr 30, 2026

Fixes #27.

Provider factories (openAI, anthropic, gemini) used createRequire(import.meta.url) with relative paths to load internal provider classes (../providers/OpenAIProvider.js). esbuild cannot statically analyze esmRequire(...) calls — they are opaque function calls, not import/require statements — so it leaves them as-is in the bundle. In the published package, out/dist/index.js is a single bundled file. The relative path ../providers/OpenAIProvider.js does not exist on disk, causing MODULE_NOT_FOUND at runtime for any consumer who calls a factory.

esmRequire remains correct for external peer dependencies (openai, @anthropic-ai/sdk, @google/generative-ai) — these are marked external in esbuild config and must resolve at runtime from the consumer's node_modules.

Root cause

createRequire(import.meta.url)('../providers/OpenAIProvider.js') inside create: callback of each factory. esbuild bundles all internal modules into one file but cannot trace dynamic esmRequire(...) calls, so the require to an internal module survives into the bundle as a broken runtime path.

Fix

Replace esmRequire('../providers/XxxProvider.js') with static ESM import { XxxProvider } at module top level. Internal modules are part of the bundle — esbuild inlines them. No lazy loading benefit was lost because src/provider/index.ts already statically exports all three provider classes, meaning they were already in the bundle regardless.

Also fixes a latent exactOptionalPropertyTypes type error that was hidden by the any-typed esmRequire return: defaults: options.defaults passed undefined to an optional property. Fixed via conditional spread.

Changes

File Change
src/provider/factories/openai.ts esmRequire('../providers/OpenAIProvider.js') → static import { OpenAIProvider }
src/provider/factories/anthropic.ts esmRequire('../providers/AnthropicProvider.js') → static import { AnthropicProvider }
src/provider/factories/gemini.ts esmRequire('../providers/GoogleGeminiProvider.js') → static import { GoogleGeminiProvider }
src/provider/factories/openai.ts defaults: options.defaults → conditional spread for exactOptionalPropertyTypes
src/provider/factories/anthropic.ts Same defaults fix
src/provider/factories/gemini.ts Same defaults fix
test/e2e/mcp-agentic-pack-e2e.ts Rewritten: 66 checks across 6 phases (was 12 checks, CLI-only)

Why the pack e2e test didn't catch this

The previous mcp-agentic-pack-e2e.ts tested only the CLI binary (mcp-agentic) — it started the bare server, connected via stdio, and verified 8 tools respond. The CLI binary does not import provider factories. No test exercised import { openAI } from '@stdiobus/mcp-agentic' from an installed tarball — the exact path that breaks.

New pack e2e coverage (66 checks)

Phase What it tests Why
A: CLI binary Start mcp-agentic, 8 tools, health check Baseline binary functionality
B: Value exports All 14 value exports resolve from installed package Catches MODULE_NOT_FOUND for any export
C: Provider factories openAI(), anthropic(), gemini() without SDK → BridgeError CONFIG Would have caught this bug — MODULE_NOT_FOUND instead of CONFIG
D: Full pipeline defineProvidercreateMultiProviderAgentMcpAgenticServer → session → prompt End-to-end user journey from installed package
E: Type declarations 13 .d.ts files exist TypeScript consumers get types
F: Bundle integrity Regex scan for broken internal requires in bundle Preventive — catches the pattern before runtime

Verified

Check Result
npm run typecheck ✓ clean
npm run test:unit ✓ 780 passed
npm run test:e2e ✓ 5 suites, all passed (239 total checks)
npm run build ✓ clean
Bundle scan ✓ no providers/ references in out/dist/index.js

Scope

4 source files changed (3 factory files + 1 e2e test). Net logic change: 3 lines replaced per factory (import + conditional spread). No public API changes. No behavior changes.

- Replace dynamic `esmRequire()` calls with static ES imports in anthropic, gemini, and openai factory modules
- Add conditional spread operator for optional `defaults` field to avoid undefined values in provider config
- Expand E2E test scope to verify published package works as both MCP server and library
- Add consumer script generator to test all public exports, type declarations, and factory functions
- Document why dynamic requires break in esbuild bundles and how this test catches such issues
- Improve test clarity with detailed pipeline steps and verification checklist
@morozow morozow self-assigned this Apr 30, 2026
@morozow morozow requested a review from a team as a code owner April 30, 2026 19:39
@morozow morozow added the bug Something isn't working label Apr 30, 2026
@morozow morozow merged commit 29f9a8d into main Apr 30, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider factories crash with MODULE_NOT_FOUND when imported from published npm package

1 participant