Skip to content

DTS output hard-imports all bundler packages, causing TS2307 for consumers with skipLibCheck: false #589

Description

@YevheniiKotyrlo

Problem

Consumers with skipLibCheck: false get TS2307 errors from dist/index.d.mts because it hard-imports types from all 10 bundler packages. If a consumer only uses one bundler (e.g., Vite), they still need webpack, @farmfe/core, @rspack/core, bun, rolldown, and unloader installed to satisfy the type imports.

This has been reported multiple times:

Reproduction

mkdir unplugin-repro && cd unplugin-repro
pnpm init && pnpm add unplugin vite typescript @types/node

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "skipLibCheck": false,
    "noEmit": true
  },
  "include": ["src"]
}

src/index.ts:

import type { UnpluginInstance } from 'unplugin'
export type { UnpluginInstance }
npx tsc --noEmit

Result: 7 TS2307 errors — 6 from unplugin's dist/index.d.mts, 1 from webpack-virtual-modules:

node_modules/.pnpm/unplugin@3.1.0/node_modules/unplugin/dist/index.d.mts:2:50 - error TS2307: Cannot find module '@farmfe/core' or its corresponding type declarations.
node_modules/.pnpm/unplugin@3.1.0/node_modules/unplugin/dist/index.d.mts:3:90 - error TS2307: Cannot find module '@rspack/core' or its corresponding type declarations.
node_modules/.pnpm/unplugin@3.1.0/node_modules/unplugin/dist/index.d.mts:4:49 - error TS2307: Cannot find module 'bun' or its corresponding type declarations.
node_modules/.pnpm/unplugin@3.1.0/node_modules/unplugin/dist/index.d.mts:6:42 - error TS2307: Cannot find module 'rolldown' or its corresponding type declarations.
node_modules/.pnpm/unplugin@3.1.0/node_modules/unplugin/dist/index.d.mts:8:43 - error TS2307: Cannot find module 'unloader' or its corresponding type declarations.
node_modules/.pnpm/unplugin@3.1.0/node_modules/unplugin/dist/index.d.mts:10:98 - error TS2307: Cannot find module 'webpack' or its corresponding type declarations.
node_modules/.pnpm/webpack-virtual-modules@0.6.2/node_modules/webpack-virtual-modules/lib/index.d.ts:1:33 - error TS2307: Cannot find module 'webpack' or its corresponding type declarations.

Root Cause

src/types.ts imports types from all bundler packages. The tsdown.config.ts externalizes them via external (now deps.neverBundle), which correctly keeps them external in the JS output — but also keeps them as hard import type statements in the DTS output. Consumers who don't install every bundler hit TS2307.

Analysis

I investigated four approaches before filing this issue:

1. Ambient declare module fallbacks

Ship a .d.ts file with declare module 'webpack' {} etc., referenced via /// <reference path> in the DTS banner.

Result: Does not work. Ambient declare module in script files globally overrides module resolution — it replaces real module types even when the real package is installed, breaking other packages (e.g., Vite depends on Rollup's full types).

2. peerDependencies + peerDependenciesMeta

Declare all bundlers as optional peer dependencies. Semantically correct, but TypeScript doesn't care about peerDependencies metadata — TS2307 remains.

3. tsdown DTS-specific dependency config

The ideal fix: keep packages external for JS (deps.neverBundle) but inline their types in DTS output. However, tsdown currently has no DTS-specific dependency configuration — deps applies uniformly to both JS and DTS. rolldown-plugin-dts also has no resolve option for selective type inlining (discussed in rolldown/rolldown-plugin-dts#106, closed without implementation).

This is the most elegant long-term solution, but it requires a feature that doesn't exist yet in tsdown/rolldown-plugin-dts.

4. Source-level type stubs

Replace import type { Plugin } from 'vite' with locally-defined minimal interfaces like type VitePlugin = { name: string }. Eliminates all external type imports.

Trade-off: This is a breaking change — consumers lose structural compatibility between unplugin's exported types and the real bundler plugin types. For example, UnpluginInstance.vite would return a { name: string } instead of Vite's real Plugin type.

Suggested Path Forward

Given that this has been open for 3+ years (#49) and affects all consumers with skipLibCheck: false (the TypeScript-recommended setting for library authors), I'd suggest one of:

  1. Short-term: Source-level minimal type stubs for bundler types that most consumers never interact with directly (e.g., @farmfe/core, bun, unloader). Keep full types for the most common ones (vite, rollup, esbuild) which are typically already installed as transitive deps.

  2. Long-term: Wait for / request DTS-specific dependency inlining in tsdown (approach 3 above).

  3. Something else — I'm happy to implement whatever approach the maintainers prefer.

I can submit a PR for whichever direction you'd like to go.

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