tsx vs jiti vs bundle-require: Runtime TypeScript Loaders (2026)
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
| Feature | tsx | jiti | bundle-require |
|---|---|---|---|
| Run TS scripts | ✅ | ❌ | ❌ |
| Load TS configs | ⚠️ (heavy) | ✅ | ✅ |
| Watch mode | ✅ | ❌ | ❌ |
| On-demand transpile | ✅ | ✅ | ❌ (bundles all) |
| CJS/ESM interop | ✅ | ✅ | ✅ |
| Module caching | ✅ | ✅ | ❌ |
| Dependency tracking | ❌ | ❌ | ✅ |
| Temp files | ❌ | Cache | Yes |
| Transpiler | esbuild | babel/sucrase | esbuild |
| Used by | Scripts | Nuxt, Vite, Tailwind | tsup, 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
nodereplacement 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.tssupport
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 →