Skip to content

Emitted .d.ts use extensionless relative imports — types break under NodeNext/Node16 (addEventListener invisible, TS2339) #704

Description

@ken-jo

Summary

The published type declarations use extensionless relative import specifiers (e.g. import { ProtocolWithEvents } from "./events"; in dist/src/app-bridge.d.ts). Under TypeScript's Node16/NodeNext module resolution, relative specifiers must carry an explicit extension, so these imports do not resolve. As a result, any type that transitively depends on one loses its members. Most visibly, the recommended bridge.addEventListener("sandboxready", …) API (which AppBridge/App inherit from ProtocolWithEvents via ./events) is invisible to consumers, producing TS2339.

This is types-only — runtime JavaScript is unaffected (the .js bundles are produced by Bun.build, which inlines relative modules, so no extensionless relative import survives at runtime).

Environment

  • @modelcontextprotocol/ext-apps 1.7.4 (current latest); the same specifiers are present on main.
  • TypeScript with "module" / "moduleResolution" = NodeNext (or Node16).

Reproduction

tsconfig.json:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["index.ts"]
}

index.ts:

import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge";
declare const bridge: AppBridge;
bridge.addEventListener("sandboxready", () => {}); // recommended API
bridge.onsandboxready = () => {};                  // declared directly on AppBridge

Run tsc --noEmit.

Actual

index.ts:6:8 - error TS2339: Property 'addEventListener' does not exist on type 'AppBridge'.

onsandboxready (declared directly on AppBridge) compiles fine — only members inherited through the unresolved ./events import disappear. skipLibCheck suppresses the error inside the .d.ts itself, but not the TS2339 it causes in consumer code.

Expected

addEventListener / removeEventListener and the other AppBridgeEventMap events resolve for NodeNext/Node16 consumers, matching the documentation (the onsandboxready / onsizechange / … setters are @deprecated in favor of addEventListener(...)).

Root cause

tsconfig.json uses "moduleResolution": "bundler", which permits extensionless relative specifiers, and the src files write relative imports without extensions (e.g. src/app-bridge.tsimport { ProtocolWithEvents } from "./events";). tsc (emitDeclarationOnly) copies specifiers verbatim into the emitted .d.ts, so the extensionless form ships. Because the project's own type-check runs under bundler resolution, the failure is never observed in-repo; it only surfaces for downstream Node16/NodeNext consumers. External SDK imports already use explicit .js (e.g. @modelcontextprotocol/sdk/shared/protocol.js) — only the relative imports are inconsistent. Affected export subpaths include ., ./app-bridge, ./react, and ./server.

Suggested fix

Add explicit .js extensions to relative imports across src (e.g. from "./events.js", from "../app.js"). This is standard ESM/NodeNext hygiene, matches the existing SDK-import style, and is compatible with both the bundler-mode type-check and the Bun.build bundling (both resolve ./x.js./x.ts). Happy to open a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions