<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/tsx-vs-jiti-vs-bundle-require-runtime-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/tsx-vs-jiti-vs-bundle-require-runtime-2026/raw.md -->
<!-- Source path: content/guides/tsx-vs-jiti-vs-bundle-require-runtime-2026.mdx -->

---
og_image: "/images/guides/tsx-vs-jiti-vs-bundle-require-runtime-2026.webp"
title: "tsx vs jiti vs bundle-require: TS Runtime 2026"
description: "Compare tsx, jiti, and bundle-require for loading TypeScript files at runtime in Node.js. Config file loading, on-demand transpilation, and esbuild bundling."
date: "2026-03-09"
authors: ["team"]
tier: 2
tags: ["nodejs", "typescript", "developer-tools", "automation"]
---

## 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](https://github.com/privatenumber/tsx) — TypeScript executor:

### Running scripts

```bash
# 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

```typescript
// 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

```typescript
// 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](https://github.com/unjs/jiti) — on-demand TypeScript loader:

### Basic usage

```typescript
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

```typescript
// 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

```typescript
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

```typescript
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](https://github.com/egoist/bundle-require) — esbuild-based config loader:

### Basic usage

```typescript
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

```typescript
// 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

```typescript
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 `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

---

## Community Adoption in 2026

**tsx** and **jiti** are both at approximately 15 million weekly downloads, but for very different reasons. tsx is downloaded by developers who run TypeScript scripts and servers directly — it shows up in `package.json` scripts like `"dev": "tsx watch src/index.ts"` and as a peer dependency in many Node.js TypeScript project templates. jiti is downloaded primarily as a transitive dependency: Nuxt, Vite, Tailwind, and PostCSS all use jiti internally to load their TypeScript configuration files. Most jiti consumers have never explicitly installed it.

**bundle-require** sits at approximately 5 million weekly downloads, driven by tsup, unbuild, and vitest — which all use it to load `*.config.ts` files. Like jiti, it is almost exclusively a transitive dependency rather than a direct development tool. Direct use cases are primarily authors building bundlers or task runners that need to load user-provided TypeScript configuration.

The practical framing for developers choosing between these tools: if you need to run a TypeScript file directly (`node script.ts` style), use tsx. If you are building a tool that needs to load a TypeScript config file provided by users, use jiti (for lazy loading) or bundle-require (for clean isolated loading with dependency tracking). These roles don't overlap — tsx is a TypeScript runtime, while jiti and bundle-require are TypeScript module loaders used within JavaScript tooling.


## Development Workflow Integration

The choice of TypeScript runtime tool has downstream effects on developer workflow that go beyond raw execution speed.

**tsx** is the go-to choice for scripts that need to run frequently during development — database seed scripts, code generators, migration runners, and CLI tools that team members run locally. Because tsx uses esbuild internally and strips types rather than checking them, it starts in milliseconds and makes TypeScript feel like a scripting language. Many teams use tsx as their default Node.js script runner even for non-library code, replacing `ts-node` entirely. The developer workflow becomes: write TypeScript, run with `tsx`, iterate. Type checking is deferred to a separate `tsc --noEmit` step in CI.

**jiti** is most commonly encountered as a transitive dependency inside build tools. If you use Vite, Nuxt, or any UnJS tool, jiti is already in your `node_modules` handling the loading of configuration files. Understanding jiti helps when debugging configuration issues — knowing that `vite.config.ts` is loaded by jiti (not by tsc, tsx, or node directly) explains why certain TypeScript features work in config files even without explicit TypeScript configuration. Jiti's `createJiti()` API with `moduleCache` enabled is also valuable in test environments where you want module loading to be fast and cached across test files.

**bundle-require** fills a specific gap that neither tsx nor jiti addresses: bundled config loading with tree-shaking and dependency collection. When a tool like Vitest or tsup loads your config file with `bundle-require`, it gets back not just the exported value but also a list of every file that the config imported. This dependency list is used to invalidate the config cache when any of those files change — enabling precise hot-reload of the tool configuration in `--watch` mode.

For most teams, tsx is the day-to-day tool and jiti/bundle-require are implementation details of the tools you use. A practical rule: if you are writing a Node.js script that you run directly, use tsx. If you are writing a build tool or configuration loader, evaluate bundle-require for its dependency tracking. If you are targeting edge runtimes or environments where esbuild is not available, jiti's broader runtime compatibility may matter.


A practical consideration when choosing between these tools is their behavior with `package.json` type field. When `"type": "module"` is set in `package.json`, Node.js treats `.js` files as ESM and `.cjs` files as CJS. tsx handles this correctly in both module systems. jiti defaults to CJS mode but supports ESM via its `esmResolve` option. bundle-require uses Rollup's module system detection. For projects that mix ESM and CJS (a common legacy state during migration), testing your chosen tool with both file types in the actual project structure is more reliable than relying on documentation — edge cases in module system detection are a frequent source of subtle runtime differences.

## 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 →](https://www.pkgpulse.com)*

The relationship between these three tools and the broader UnJS ecosystem is worth understanding. jiti is a core piece of the UnJS stack — the same organization that maintains H3, Nitro, unstorage, ofetch, and ufo. When Nuxt adopted Nitro as its server engine, jiti became the standard mechanism for loading TypeScript configuration in JavaScript tooling across the entire UnJS ecosystem. This means that for developers on the UnJS stack (Nuxt, H3, Nitro, or custom tools built with UnJS utilities), jiti is already present as a transitive dependency and its API is well-supported. Adding `createJiti()` calls for custom configuration loading in a Nuxt plugin or a Nitro server plugin is the idiomatic approach, rather than introducing tsx or bundle-require as separate tools.

A subtle interplay between these three tools affects projects that use path aliases (`@/` or `~/` mapped to `src/` in tsconfig). tsx reads `tsconfig.json` and resolves path aliases via esbuild's tsconfig-paths support, so `import { db } from "@/lib/db"` works out of the box when running scripts with tsx. jiti's alias support requires explicit configuration via the `alias` option in `createJiti()` — it does not automatically read tsconfig path mappings. bundle-require passes through esbuild options including the tsconfig path, so path aliases work if you pass the correct `tsconfig` option. For monorepos where workspace packages are referenced by path alias, testing that your toolchain resolves aliases correctly before committing is worthwhile — silent resolution failures manifest as "Cannot find module '@/lib/db'" errors that look like missing files rather than configuration issues.

*See also: [Bun vs Vite](/compare/bun-vs-vite) and [cac vs meow vs arg 2026](/guides/cac-vs-meow-vs-arg-lightweight-cli-argument-parsers-2026), [archiver vs adm-zip vs JSZip (2026)](/guides/archiver-vs-adm-zip-vs-jszip-zip-archive-creation-2026).*
