A JIT compiler for running TypeScript and JavaScript code in Node.js without compilation, built on top of SWC with full ESM support for Node.js 24 and above.
ts-exec is a TypeScript execution engine that lets you run .ts and .tsx files directly in Node.js without a build step. Unlike other popular solutions, ts-exec prioritizes compatibility with Node.js module resolution, ensuring that code running with ts-exec will work identically after compilation to JavaScript.
Existing solutions like ts-node and tsx have served the community well, but they come with significant tradeoffs that can lead to production issues.
ts-node is no longer actively maintained and has become bloated over time. While tsx offers a modern alternative, it makes a critical compromise: it allows extension-less imports and directory imports during development. This creates a dangerous disconnect. Your code runs perfectly in development but breaks in production when compiled to JavaScript, because Node.js strictly requires explicit file extensions and cannot resolve directory imports.
ts-exec takes a different approach. It provides the complete TypeScript feature set (including enums, legacy decorators, and JSX syntax) while strictly following Node.js file resolution rules. This means every import that works with ts-exec will work after compilation, eliminating an entire class of production deployment failures.
|
π¨ Full TypeScript support |
π‘οΈ Production-safe imports |
|
π¦ Built for ESM |
β‘ Modern foundation |
|
πͺΆ Lightweight |
βοΈ Zero configuration |
|
β
Development confidence |
npm i -D @poppinss/ts-execRun any TypeScript file directly.
node --import=@poppinss/ts-exec ./src/index.tsUse ts-exec within your Node.js application.
import '@poppinss/ts-exec'
// Now you can import TypeScript files
const module = await import('./my-typescript-file.ts')TypeScript originally made a deliberate decision to keep import paths unchanged during compilation. When you write an import in TypeScript, it stays exactly the same in the compiled JavaScript output. This design choice means you must reference the file extension that will exist after compilation, not the current TypeScript extension.
For example, when importing a TypeScript file, you write the import with a .js extension because that's what will exist after compilation.
export class UserService {
// implementation
}// Reference with .js even though the file is user_service.ts
import { UserService } from '../services/user_service.js'After compilation, both files become .js files, and the import path works seamlessly with Node.js without any modifications.
Recently, the TypeScript team introduced the rewriteRelativeImportExtensions compiler option, which allows you to use .ts extensions in your imports. When this option is enabled, TypeScript will rewrite .ts extensions to .js during compilation.
If you prefer to use .ts extensions in your imports, enable this option in your tsconfig.json.
{
"compilerOptions": {
"rewriteRelativeImportExtensions": true
}
}With this configuration, you can write imports using .ts extensions.
// Now you can use .ts extension
import { UserService } from '../services/user_service.ts'TypeScript will automatically rewrite this to .js during compilation, and ts-exec will handle it correctly during development.
Subpath import aliases are defined in your package.json file, which remains unchanged during the TypeScript compilation process. Since package.json is used directly by Node.js at runtime and is never rewritten by the TypeScript compiler, the paths you define in your aliases must reference the compiled output extensions.
This means your subpath aliases should always use .js extensions, even when the source files are .ts. The package.json file will be the same in both development (with ts-exec) and production (with compiled JavaScript), so the paths need to work for the compiled output.
{
"name": "my-app",
"imports": {
"#controllers/*": "./build/controllers/*.js",
"#services/*": "./build/services/*.js",
"#models/*": "./build/models/*.js"
}
}With these aliases defined, you can use them in your TypeScript source files.
import { UsersController } from '#controllers/users_controller'
import { UserService } from '#services/user_service'
import { User } from '#models/user'This approach works seamlessly with both ts-exec during development and Node.js after compilation. The aliases resolve correctly in both environments because package.json remains unchanged and Node.js uses it directly for module resolution.
If you have rewriteRelativeImportExtensions enabled in your TypeScript configuration, it will not affect subpath import aliases. The rewriting only applies to relative imports (those starting with ./ or ../), not to bare specifiers or subpath imports.
If you're currently using tsx, you might wonder why to switch. The answer depends on your priorities.
tsx is excellent and fast, but it allows patterns that will fail in production. Consider this common scenario:
| β Works with tsx (breaks after compilation) | β Works with ts-exec (works after compilation) |
|---|---|
// Extension-less import
import { User } from './models/user'
// Directory import
import config from './config'
// After compilation:
// Error: Cannot find module |
// Explicit extension
import { User } from './models/user.ts'
// Explicit file
import config from './config/index.ts'
// After compilation:
// Works identically β¨ |
Additionally, tsx doesn't support legacy decorators, which many projects still rely on. ts-exec provides full decorator support, making it compatible with frameworks and libraries that haven't migrated to the new decorator specification.
| Feature | ts-exec | tsx | ts-node | Node.js (native) |
|---|---|---|---|---|
| Active maintenance | β | β | β | β |
| Built on SWC | β | β | β | |
| Legacy decorators | β | β | β | β |
| Node.js-compliant imports | β | β | β | β |
| ESM-first | β | β | β | |
| Full TypeScript features | β | β | β | |
| Respects tsconfig.json | β | β | β | β |
| Lightweight | β | β | β | β |
One of the primary goals of Poppinss is to have a vibrant community of users and contributors who believes in the principles of the framework.
We encourage you to read the contribution guide before contributing to the framework.
In order to ensure that the poppinss community is welcoming to all, please review and abide by the Code of Conduct.
@poppinss/ts-exec is open-sourced software licensed under the MIT license.