tsx Documentation

️ TypeScript Execute | The easiest way to run TypeScript in Node.js

12,069 stars TypeScript TypeScript 1 file · ~3,140 tokens #cli#esbuild#esm#loader#node#runtime#typescript#watch
RAW Doc

File: README.md

Sponsors


File: docs/dev-api/entry-point.md

Entry-point

Import tsx at the top of your entry-file:

js
import 'tsx'

// Now you can load TS files
await import('./file.ts')

Note, because of the order of static import evaluation in ESM, the enhancement only works on dynamic imports after registration.

::: danger
Enhancing Node.js by loading tsx from within your source code at run-time can be unexpected for collaborators who aren’t aware of it.

When possible, it's recommended to use a more visible method, such as passing it as a CLI flag.
:::

Advanced usage

CommonJS mode only

Require tsx/cjs at the top of your entry-file:

js
require('tsx/cjs')

// Now you can load TS files
require('./file.ts')

Module mode only

Import tsx/esm at the top of your entry-file:

js
import 'tsx/esm'

// Now you can load TS files
await import('./file.ts')

File: docs/dev-api/index.md

Developer API

The Developer API allows you to enhance Node.js with tsx without needing to use the tsx command.

Note that CLI features such as Watch mode and the REPL will not be available.

Use-cases

Directly running `node`

Sometimes, you may need to run node directly but still want to use tsx. Instead of using the tsx command, you can pass it to Node with node --import tsx or you can do import 'tsx' at the top of your script. This is helpful when you need more control over the Node.js environment, are integrating with tools that specifically call node, or simply prefer using the node command.

3rd party packages

Some third-party packages need to load TypeScript files, like configuration files, without affecting the entire runtime environment. The tsImport() API allows these packages to load TypeScript files natively, without causing side effects to the environment.

Understanding module types

Understanding the Node.js module types, CommonJS (CJS) and ES Module (ESM), can be helpful when using the Developer API.

ESM is the modern standard, indicated by the file extensions .mjs for JavaScript and .mts for TypeScript. CommonJS, the older format, uses the file extensions .cjs for JavaScript and .cts for TypeScript. When the file extension is ambiguous, such as .js or .ts, package.json#type is used to determine the module type. If type is set to module, the files are treated as ES Modules. If type is set to commonjs or not set at all, the files are treated as CommonJS modules.

Although both module types can coexist in the same environment, they have distinct scopes and behaviors. tsx offers APIs to enhance both CommonJS and ES modules selectively. Being aware of these distinctions and knowing which module type you are using will allow you to make informed decisions when opting into these enhancements.


File: docs/dev-api/node-cli.md

Node.js CLI

To use the node command directly with tsx, pass it as a flag:

sh
node --import tsx ./file.ts

::: details (Deprecated) Node.js v20.5.1 and below

Older Node.js versions use a deprecated API --loader instead of --import.

sh
node --loader tsx ./file.ts

:::

Custom `tsconfig.json` path

To specify a custom path to tsconfig.json, use an environment variable:

sh
TSX_TSCONFIG_PATH=./path/to/tsconfig.custom.json node --import tsx ./file.ts

Binaries

If you don't have direct access to the node command, use the Node.js NODE_OPTIONS environment variable to pass in the flag:

sh
NODE_OPTIONS='--import tsx' npx some-binary

Advanced usage

CommonJS mode only

To use tsx for CommonJS files only:

sh
node --require tsx/cjs ./file.ts

Module mode only

To use tsx for Module files only:

sh
node --import tsx/esm ./file.ts

File: docs/dev-api/register-cjs.md

CommonJS Register API

The CommonJS Register API allows you to manually register the enhancement at runtime. But note, this only affects CommonJS modules (.cjs/.cts, and .js/.ts when package.json#type is unset or commonjs).

::: warning Caveats

  • import() calls in the loaded files are not enhanced because they're handled by Node's ESM hook. Use with the ESM Register API.
  • Because it compiles ESM syntax to run in CommonJS mode, top-level await is not supported
    :::

Usage

js
const tsx = require('tsx/cjs/api')

// Register tsx enhancement
const unregister = tsx.register()

const loaded = require('./file.ts')

// Unregister when needed
unregister()

Scoped registration

If you want to register without affecting the entire runtime environment, you can add a namespace.

When a namespace is provided, it will return a private require() method for you to load files with:

js
const crypto = require('node:crypto')
const tsx = require('tsx/cjs/api')

const api = tsx.register({
    // Pass in a unique namespace
    namespace: crypto.randomUUID()
})

// Pass in the request and the current file path
const loaded = api.require('./file.ts', __filename)

api.unregister()

File: docs/dev-api/register-esm.md

ESM Register API

The ESM Register API allows you to manually register the enhancement at runtime. But note, this only affects ESM (.mjs/.mts, and .js/.ts when package.json#type is module).

Usage

js
import { register } from 'tsx/esm/api'

// Register tsx enhancement
const unregister = register()

await import('./file.ts')

// Unregister when needed
unregister()

Scoped registration

If you want to register without affecting the entire runtime environment, you can add a namespace.

When a namespace is provided, it will return a private import() method for you to load files with:

js
import crypto from 'node:crypto'
import { register } from 'tsx/esm/api'

const api = register({
    // Pass in a unique namespace
    namespace: crypto.randomUUID()
})

// Pass in the request and the current file path
// Since this is namespaced, it will not cache hit from prior imports
const loaded = await api.import('./file.ts', import.meta.url)

// This is using the same namespace as above, so it will yield a cache hit
const loaded2 = await api.import('./file.ts', import.meta.url)

api.unregister()

Tracking loaded files

Detect files that get loaded with the onImport hook. This can be useful when you want to track dependencies when setting up a watcher.

ts
register({
    onImport: (file: string) => {
        console.log(file) // 'file:///foo.ts'
    }
})

File: docs/dev-api/ts-import.md

`tsImport()`

Native dynamic import() function to import TypeScript files (supports top-level await).

Use this function for importing TypeScript files without adding TypeScript support to the entire runtime.

The current file path must be passed in as the second argument to resolve the import context.

Since this is designed for one-time use, it does not cache loaded modules.

Usage

ESM

js
import { tsImport } from 'tsx/esm/api'

const loaded = await tsImport('./file.ts', import.meta.url)

// If tsImport is used to load file.ts again,
// it does not yield a cache-hit and re-loads it
const loadedAgain = await tsImport('./file.ts', import.meta.url)

If you'd like to leverage module caching, see the ESM scoped registration section.

CommonJS

js
const { tsImport } = require('tsx/esm/api')

const loaded = await tsImport('./file.ts', __filename)

`tsconfig.json`

Custom `tsconfig.json` path

ts
tsImport('./file.ts', {
    parentURL: import.meta.url,
    tsconfig: './custom-tsconfig.json'
})

Disable `tsconfig.json` lookup

ts
tsImport('./file.ts', {
    parentURL: import.meta.url,
    tsconfig: false
})

Tracking loaded files

Detect files that get loaded with the onImport hook:

ts
tsImport('./file.ts', {
    parentURL: import.meta.url,
    onImport: (file: string) => {
        console.log(file)
        // file:///foo.ts
    }
})

File: docs/dev-api/tsx-require.md

`tsx.require()`

Native require() function enhanced with TypeScript & ESM support.

Use this function for importing TypeScript files in CommonJS mode without adding TypeScript support to the entire runtime.

Note, the current file path must be passed in as the second argument to resolve the import context.

::: warning Caveats

  • import() calls in the loaded files are not enhanced. Use tsImport() instead.
  • Because it compiles ESM syntax to run in CommonJS mode, top-level await is not supported
    :::

Usage

CommonJS

js
const tsx = require('tsx/cjs/api')

const tsLoaded = tsx.require('./file.ts', __filename)
const tsFilepath = tsx.require.resolve('./file.ts', __filename)

ESM

js
import { require } from 'tsx/cjs/api'

const tsLoaded = require('./file.ts', import.meta.url)
const tsFilepath = require.resolve('./file.ts', import.meta.url)

Tracking loaded files

Because the CommonJS API tracks loaded modules in require.cache, you can use it to identify loaded files for dependency tracking. This can be useful when implementing a watcher.

js
const resolvedPath = tsx.require.resolve('./file', import.meta.url)

const collectDependencies = module => [
    module.filename,
    ...module.children.flatMap(collectDependencies)
]

console.log(collectDependencies(tsx.require.cache[resolvedPath]))
// ['/file.ts', '/foo.ts', '/bar.ts']

File: docs/compilation.md

Compilation

Compiling your TypeScript files to JavaScript is not handled by tsx, but it's a necessary step in most setups.

::: info Should I publish TypeScript files?

No. While tsx is capable of running TypeScript files in dependencies if need be (e.g. monorepos), it's highly discouraged to publish uncompiled TypeScript. Source files require a specific compilation configuration in tsconfig.json which may not be read, and TypeScript performance will degrade.
:::

Compiling an npm package

npm packages are distinguished from applications by defining package entry-points in package.json.

pkgroll is the recommended bundler for projects using tsx. It's developed by the same author and used to compile tsx.

Given your source files are in the src directory, it automatically infers how to build your package based on the entry points defined in package.json by outputting them to the dist directory.

Setup

::: details Basic setup

Set your entry-point in exports:

json5
// package.json
{
    "exports": "./dist/index.mjs"
}

:::

::: details Dual package (CJS & ESM)

Set your CommonJS & Module entry-points in exports:

json5
// package.json
{
    "main": "./dist/index.cjs",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.cts",

    "exports": {
        "require": {
            "types": "./dist/index.d.cts",
            "default": "./dist/index.cjs"
        },
        "import": {
            "types": "./dist/index.d.mts",
            "default": "./dist/index.mjs"
        }
    }
}

:::

::: details Command-line package

Set your CLI entry-point in bin:

json5
// package.json
{
    "bin": "./dist/cli.mjs"
}

:::

Build

In your directory, simply run pkgroll:

::: code-group

sh [npm]
$ npx pkgroll
sh [pnpm]
$ pnpm pkgroll
sh [yarn]
$ yarn pkgroll

:::

Optionally, add a build script for convenience:

json
// package.json
{
    // Optional: build script
    "scripts": {// [!code ++]
        "build": "pkgroll"// [!code ++]
    }// [!code ++]
}

File: docs/contact.md

Contact