Skip to content

poppinss/ts-exec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

44 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

πŸš€ @poppinss/ts-exec

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.

gh-workflow-image npm-image license-image



🎯 Overview

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.


πŸ’‘ Why ts-exec exists

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.


✨ Features

🎨 Full TypeScript support
Enums, legacy decorators, namespace imports, and JSX syntax

πŸ›‘οΈ Production-safe imports
Enforces Node.js module resolution

πŸ“¦ Built for ESM
First-class support for ECMAScript modules

⚑ Modern foundation
Built on SWC for fast compilation

πŸͺΆ Lightweight
Less than 200 lines, inspired by Amaro

βš™οΈ Zero configuration
No custom config files needed, parses tsconfig.json automatically

βœ… Development confidence
Code that runs with ts-exec will run after compilation


πŸ“¦ Installation

npm i -D @poppinss/ts-exec

🚦 Usage

Basic execution

Run any TypeScript file directly.

node --import=@poppinss/ts-exec ./src/index.ts

Programmatic usage

Use 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')

πŸ“ Imports with .ts file extension

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

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.


πŸ€” Why not tsx?

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.


πŸ“Š Comparison

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 βœ… βœ… ❌ βœ…


🀝 Contributing

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.


πŸ“œ Code of Conduct

In order to ensure that the poppinss community is welcoming to all, please review and abide by the Code of Conduct.


πŸ“„ License

@poppinss/ts-exec is open-sourced software licensed under the MIT license.

About

Execute TypeScript on Node using SWC

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published