Skip to main content

Guide

tsx vs jiti vs bundle-require: TS Runtime 2026

Compare tsx, jiti, and bundle-require for loading TypeScript files at runtime in Node.js. Config file loading, on-demand transpilation, and esbuild bundling.

·PkgPulse Team·
0

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

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 →

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 and cac vs meow vs arg 2026, archiver vs adm-zip vs JSZip (2026).

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.