Migration Guide: TSDX v0.x to v2.0

December 29, 2025 ยท View on GitHub

This guide helps you migrate from the original TSDX (v0.x) to the modern TSDX 2.0.

What's Changed

TSDX 2.0 is a complete rewrite that replaces the original toolchain with modern, high-performance alternatives:

Old (v0.x)New (v2.0)Why
Rollup + BabelbuncheeZero-config, SWC-powered, faster
JestvitestVite-native, faster, Jest-compatible
ESLintoxlint50-100x faster, Rust-powered
Prettieroxfmt35x faster, Rust-powered
yarn/npmbunFaster installs and execution
Node 10+Node 20+LTS only

Quick Migration

For most projects, follow these steps:

1. Install Bun

# macOS/Linux
curl -fsSL https://bun.sh/install | bash

# Windows
powershell -c "irm bun.sh/install.ps1 | iex"

2. Update package.json

Replace your scripts:

{
  "scripts": {
    "dev": "tsdx dev",
    "build": "tsdx build",
    "test": "tsdx test",
    "lint": "tsdx lint",
    "format": "tsdx format",
    "typecheck": "tsdx typecheck",
    "prepublishOnly": "bun run build"
  }
}

3. Update Dependencies

Remove old dependencies and add new ones:

# Remove old dependencies
bun remove tsdx rollup @rollup/plugin-* babel-* @babel/* jest ts-jest eslint @typescript-eslint/* prettier husky lint-staged

# Add new tsdx
bun add -D tsdx typescript

4. Replace Jest with Vitest

Create vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node', // or 'jsdom' for React/DOM testing
  },
});

Update test files:

  • Change import { describe, it, expect } from 'jest' to import { describe, it, expect } from 'vitest'
  • Or use globals: true in vitest.config.ts to avoid imports

Jest to Vitest Cheatsheet:

JestVitest
jest.fn()vi.fn()
jest.mock()vi.mock()
jest.spyOn()vi.spyOn()
jest.useFakeTimers()vi.useFakeTimers()
beforeAll/afterAllSame
beforeEach/afterEachSame
describe/it/testSame
expect()Same

5. Remove Old Config Files

Delete these files (they're no longer needed):

rm -f tsdx.config.js
rm -f jest.config.js
rm -f .babelrc babel.config.js babel.config.json
rm -f .eslintrc .eslintrc.js .eslintrc.json .eslintignore
rm -f .prettierrc .prettierrc.js .prettierrc.json .prettierignore
rm -f rollup.config.js
rm -f yarn.lock package-lock.json

6. Update tsconfig.json

Update to modern settings:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

7. Update package.json Exports

Ensure your package.json has modern exports:

{
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./package.json": "./package.json"
  },
  "files": ["dist", "src"],
  "engines": {
    "node": ">=20"
  }
}

8. Install and Test

# Install dependencies
bun install

# Run tests
bun run test

# Build
bun run build

# Lint
bun run lint

Detailed Migration

Build Configuration

Old TSDX (tsdx.config.js):

module.exports = {
  rollup(config, options) {
    // Custom rollup config
    return config;
  },
};

New TSDX: No configuration needed! bunchee reads your package.json exports field.

For advanced customization, create bunchee.config.ts:

import { BuncheeConfig } from 'bunchee';

export default {
  // See bunchee documentation
} satisfies BuncheeConfig;

Testing

Old Jest test:

import { sum } from './index';

describe('sum', () => {
  it('adds numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });
});

New Vitest test (same syntax!):

import { describe, it, expect } from 'vitest';
import { sum } from './index';

describe('sum', () => {
  it('adds numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });
});

Or with globals: true in vitest.config.ts:

import { sum } from './index';

describe('sum', () => {
  it('adds numbers', () => {
    expect(sum(1, 2)).toBe(3);
  });
});

React Testing

Old (enzyme/react-testing-library with Jest):

import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';

test('renders', () => {
  render(<MyComponent />);
  expect(screen.getByText('Hello')).toBeInTheDocument();
});

New (same, just with Vitest!):

import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MyComponent } from './MyComponent';

describe('MyComponent', () => {
  it('renders', () => {
    render(<MyComponent />);
    expect(screen.getByText('Hello')).toBeDefined();
  });
});

Note: Replace toBeInTheDocument() with toBeDefined() or add @testing-library/jest-dom and configure in vitest setup.

Linting

Old ESLint (.eslintrc.js):

module.exports = {
  extends: ['react-app', 'prettier'],
  rules: {
    'no-unused-vars': 'warn',
  },
};

New oxlint (.oxlintrc.json) - optional:

{
  "rules": {
    "no-unused-vars": "warn"
  }
}

Most ESLint rules have equivalents in oxlint. Check oxlint rules documentation.

Formatting

Old Prettier (.prettierrc):

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2
}

New oxfmt (.oxfmtrc.json) - optional:

{
  "indentWidth": 2,
  "lineWidth": 100
}

GitHub Actions

Old workflow:

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [12, 14, 16]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}
      - run: yarn install
      - run: yarn build
      - run: yarn test

New workflow:

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        node: ['20', '22']
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: bun install
      - run: bun run lint
      - run: bun run typecheck
      - run: bun run build
      - run: bun run test

Breaking Changes

Removed Features

  1. Storybook template - Use Storybook CLI directly
  2. Custom Rollup config - Use bunchee config or raw rollup if needed
  3. tsdx lint - Now wraps oxlint instead of ESLint
  4. Node.js < 20 - Only Node.js 20+ (LTS) is supported

Changed Behavior

  1. Build output - Slightly different but compatible
  2. Watch mode - Now uses bunchee's watch, may have different behavior
  3. Test runner - Vitest instead of Jest (mostly compatible API)
  4. Default branch - Uses main instead of master in templates

Compatibility

Your Library Consumers

No changes needed! The build output format is compatible:

  • ESM and CommonJS dual publish
  • TypeScript declarations
  • Same export patterns

Your Development Workflow

TaskOld CommandNew Command
Create projectnpx tsdx create mylibbunx tsdx create mylib
Developmentyarn startbun run dev
Buildyarn buildbun run build
Testyarn testbun run test
Lintyarn lintbun run lint
Formatyarn prettier --write .bun run format

Troubleshooting

"bun: command not found"

Install bun:

curl -fsSL https://bun.sh/install | bash

Tests fail with "vi is not defined"

Add vitest imports:

import { describe, it, expect, vi } from 'vitest';

Or enable globals in vitest.config.ts:

export default defineConfig({
  test: {
    globals: true,
  },
});

TypeScript errors with moduleResolution

Update tsconfig.json:

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

ESM/CJS interop issues

Ensure your package.json has:

{
  "type": "module"
}

"Cannot find module" in tests

Check your vitest.config.ts has correct paths:

export default defineConfig({
  test: {
    include: ['test/**/*.test.ts'],
  },
});

Getting Help