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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions