Skip to main content

tsx vs jiti vs bundle-require: Runtime TypeScript Loaders (2026)

·PkgPulse Team

TL;DR

tsx is the TypeScript executor — runs any .ts file directly using esbuild, drop-in replacement for node. jiti is the on-demand TypeScript/ESM transpiler — loads config files and modules at runtime, handles CJS/ESM interop, used by Nuxt, Vite, and Tailwind for their config files. bundle-require bundles a TypeScript file with esbuild before require-ing it — used by tsup and unbuild to load tsup.config.ts files. In 2026: tsx for running TypeScript scripts, jiti for loading TypeScript config files in tools, bundle-require for one-shot config bundling.

Key Takeaways

  • tsx: ~15M weekly downloads — full TypeScript executor, watch mode, esbuild-powered
  • jiti: ~15M weekly downloads — on-demand transpilation, CJS/ESM interop, Nuxt/Vite/Tailwind config
  • bundle-require: ~5M weekly downloads — esbuild bundle + require, used by tsup, unbuild
  • Different use cases: tsx runs scripts, jiti loads modules on-demand, bundle-require bundles once
  • jiti transpiles lazily (only when imported) — minimal startup overhead
  • bundle-require bundles eagerly — produces a single file, then loads it

Use Cases

tsx:
  Run TypeScript directly → tsx src/server.ts
  Script execution → tsx scripts/migrate.ts
  Watch mode → tsx watch src/server.ts

jiti:
  Load TypeScript config files → jiti("./vite.config.ts")
  On-demand module loading → jiti("./src/utils.ts")
  CJS/ESM interop → load ESM modules from CJS context

bundle-require:
  Bundle config files → bundleRequire({ filepath: "./tsup.config.ts" })
  One-shot loading → bundle + eval, no persistent transpiler
  Clean dependency resolution → esbuild resolves all imports

tsx

tsx — TypeScript executor:

Running scripts

# Run any TypeScript file:
tsx src/index.ts
tsx scripts/seed-database.ts

# Watch mode:
tsx watch src/server.ts

# With Node.js flags:
tsx --inspect src/server.ts

# As a loader (Node.js 18+):
node --import tsx src/index.ts

As a programmatic loader

// Register tsx as a loader in your tool:
import { register } from "tsx/esm/api"

// Now TypeScript files can be imported:
register()
const config = await import("./config.ts")

tsx for config loading

// Some tools use tsx to load config files:
import { pathToFileURL } from "node:url"

// Load a TypeScript config:
async function loadConfig(configPath: string) {
  // Ensure tsx is registered:
  await import("tsx/esm/api").then((m) => m.register())

  // Now import TypeScript:
  const config = await import(pathToFileURL(configPath).href)
  return config.default ?? config
}

When tsx is overkill

tsx is a full TypeScript executor:
  ✅ Runs entire TypeScript applications
  ✅ Watch mode with fast restart
  ✅ Supports all TypeScript features
  ✅ Works as node replacement

For loading config files in a tool:
  ❌ Overkill — registers a global loader
  ❌ May conflict with other loaders
  ❌ Startup overhead for just loading one file
  → Use jiti or bundle-require instead

jiti

jiti — on-demand TypeScript loader:

Basic usage

import { createJiti } from "jiti"

const jiti = createJiti(import.meta.url)

// Load a TypeScript file on-demand:
const config = await jiti.import("./vite.config.ts")

// Load CommonJS TypeScript:
const legacyConfig = jiti("./webpack.config.ts")

// Works with any file type:
const tsModule = await jiti.import("./src/utils.ts")
const jsonData = await jiti.import("./data.json")

How tools use jiti

// This is how Nuxt loads nuxt.config.ts:
import { createJiti } from "jiti"

async function loadNuxtConfig(rootDir: string) {
  const jiti = createJiti(rootDir)

  // Try TypeScript config first, then JavaScript:
  const config = await jiti.import("./nuxt.config.ts").catch(() =>
    jiti.import("./nuxt.config.js")
  )

  return config.default ?? config
}

// Vite uses jiti to load vite.config.ts
// Tailwind uses jiti to load tailwind.config.ts
// PostCSS uses jiti to load postcss.config.ts

CJS/ESM interop

import { createJiti } from "jiti"

const jiti = createJiti(import.meta.url)

// jiti handles CJS/ESM interop transparently:

// Load ESM module from CJS context:
const esmModule = await jiti.import("esm-only-package")

// Load CJS module from ESM context:
const cjsModule = await jiti.import("./legacy-cjs-module.js")

// Load TypeScript with mixed import/require:
const mixed = await jiti.import("./config.ts")
// Works regardless of whether config.ts uses import or require

Configuration

import { createJiti } from "jiti"

const jiti = createJiti(import.meta.url, {
  // Cache transpiled files:
  fsCache: true,           // Cache to node_modules/.cache/jiti

  // Module resolution:
  moduleCache: true,       // Cache loaded modules
  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],

  // Transform options:
  interopDefault: true,    // Auto-resolve default exports
  sourceMaps: false,       // Disable source maps for speed
})

jiti vs tsx as loaders

jiti:
  ✅ On-demand — only transpiles what's imported
  ✅ No global loader registration needed
  ✅ CJS/ESM interop built-in
  ✅ Caching — transpiled files cached to disk
  ✅ Used by Nuxt, Vite, Tailwind, PostCSS
  ✅ Minimal — loads just the config, not the whole app

tsx:
  ✅ Full TypeScript executor (scripts, servers)
  ✅ Watch mode
  ✅ Source maps
  ❌ Global loader — may conflict with other tools
  ❌ Heavier — designed for running applications

bundle-require

bundle-require — esbuild-based config loader:

Basic usage

import { bundleRequire } from "bundle-require"

// Bundle and load a TypeScript config:
const { mod } = await bundleRequire({
  filepath: "./tsup.config.ts",
})

// mod is the module export:
const config = mod.default ?? mod

How tsup uses it

// tsup loads tsup.config.ts using bundle-require:
import { bundleRequire } from "bundle-require"

async function loadTsupConfig() {
  const { mod } = await bundleRequire({
    filepath: "./tsup.config.ts",
    cwd: process.cwd(),
  })

  return mod.default ?? mod
}

// Why bundle-require instead of jiti?
// → esbuild resolves and bundles ALL imports
// → Produces a single file with no external dependencies
// → Clean isolated execution

Configuration

import { bundleRequire } from "bundle-require"

const { mod, dependencies } = await bundleRequire({
  filepath: "./config.ts",

  // esbuild options:
  esbuildOptions: {
    external: ["vite"],     // Don't bundle these
    platform: "node",
    target: "node18",
  },

  // CJS or ESM output:
  format: "esm",  // or "cjs"
})

// dependencies — list of files the config imports:
console.log(dependencies)
// → ["./src/shared-types.ts", "./src/constants.ts"]
// Useful for watching config dependencies for changes

bundle-require vs jiti

bundle-require:
  ✅ Full esbuild bundle — resolves ALL imports
  ✅ Clean execution — no lingering loaders
  ✅ Returns dependency list (for file watching)
  ✅ Consistent behavior — esbuild handles everything
  ❌ Slower — bundles entire dependency tree
  ❌ Writes temp file to disk

jiti:
  ✅ Faster — only transpiles what's needed
  ✅ No temp files
  ✅ Persistent module cache
  ✅ CJS/ESM interop
  ❌ May have module resolution edge cases
  ❌ No dependency tracking

Feature Comparison

Featuretsxjitibundle-require
Run TS scripts
Load TS configs⚠️ (heavy)
Watch mode
On-demand transpile❌ (bundles all)
CJS/ESM interop
Module caching
Dependency tracking
Temp filesCacheYes
Transpileresbuildbabel/sucraseesbuild
Used byScriptsNuxt, Vite, Tailwindtsup, unbuild
Weekly downloads~15M~15M~5M

When to Use Each

Use tsx if:

  • Running TypeScript scripts and servers directly
  • Need watch mode for development
  • Want a drop-in node replacement for TypeScript
  • Running one-off scripts (tsx scripts/migrate.ts)

Use jiti if:

  • Building a tool that loads TypeScript config files
  • Need on-demand transpilation (lazy, only what's imported)
  • Want CJS/ESM interop for config loading
  • Building Nuxt/Vite/Tailwind-style tools with .config.ts support

Use bundle-require if:

  • Need a clean one-shot config load (bundle → eval → done)
  • Want dependency tracking (know which files the config imports)
  • Building a bundler/build tool that loads *.config.ts
  • Need full esbuild resolution for config imports

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on tsx v4.x, jiti v2.x, and bundle-require v5.x.

Compare TypeScript tooling and developer utilities on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.