Showing preview only (291K chars total). Download the full file or copy to clipboard to get everything.
Repository: privatenumber/tsx
Branch: master
Commit: 938e46c21e2f
Files: 127
Total size: 263.6 KB
Directory structure:
gitextract_cdughvzf/
├── .editorconfig
├── .github/
│ ├── DISCUSSION_TEMPLATE/
│ │ └── q-a.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.yml
│ │ └── config.yml
│ ├── renovate.json
│ └── workflows/
│ ├── lock-threads.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .nvmrc
├── .vscode/
│ └── settings.json
├── CONTRIBUTING.md
├── FUNDING.json
├── LICENSE
├── README.md
├── docs/
│ ├── .vitepress/
│ │ ├── config.ts
│ │ └── theme/
│ │ ├── components/
│ │ │ ├── AsideSponsors.vue
│ │ │ ├── ContactForm.vue
│ │ │ ├── ImageLink.vue
│ │ │ ├── Marquee.vue
│ │ │ └── Sponsors.vue
│ │ ├── index.ts
│ │ └── styles.css
│ ├── compilation.md
│ ├── contact.md
│ ├── dev-api/
│ │ ├── entry-point.md
│ │ ├── index.md
│ │ ├── node-cli.md
│ │ ├── register-cjs.md
│ │ ├── register-esm.md
│ │ ├── ts-import.md
│ │ └── tsx-require.md
│ ├── faq.md
│ ├── getting-started.md
│ ├── index.md
│ ├── learn.md
│ ├── node-enhancement.md
│ ├── package.json
│ ├── postcss.config.js
│ ├── scripts/
│ │ └── hash-class-names.js
│ ├── shell-scripts.md
│ ├── tailwind.config.js
│ ├── typescript.md
│ ├── vscode.md
│ └── watch-mode.md
├── package.json
├── pnpm-workspace.yaml
├── release.config.cjs
├── src/
│ ├── @types/
│ │ ├── es-module-lexer.d.ts
│ │ └── module.d.ts
│ ├── cjs/
│ │ ├── api/
│ │ │ ├── index.ts
│ │ │ ├── module-extensions.ts
│ │ │ ├── module-resolve-filename/
│ │ │ │ ├── index.ts
│ │ │ │ ├── interop-cjs-exports.ts
│ │ │ │ ├── is-from-cjs-lexer.ts
│ │ │ │ ├── preserve-query.ts
│ │ │ │ ├── resolve-implicit-extensions.ts
│ │ │ │ └── resolve-ts-extensions.ts
│ │ │ ├── register.ts
│ │ │ ├── require.ts
│ │ │ └── types.ts
│ │ └── index.ts
│ ├── cli.ts
│ ├── esm/
│ │ ├── api/
│ │ │ ├── index.ts
│ │ │ ├── register.ts
│ │ │ ├── scoped-import.ts
│ │ │ └── ts-import.ts
│ │ ├── hook/
│ │ │ ├── index.ts
│ │ │ ├── initialize.ts
│ │ │ ├── load.ts
│ │ │ ├── package-json.ts
│ │ │ ├── resolve.ts
│ │ │ └── utils.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── loader.ts
│ ├── patch-repl.ts
│ ├── preflight.cts
│ ├── remove-argv-flags.ts
│ ├── repl.ts
│ ├── run.ts
│ ├── source-map.ts
│ ├── suppress-warnings.cts
│ ├── types.ts
│ ├── utils/
│ │ ├── debug.ts
│ │ ├── es-module-lexer.ts
│ │ ├── ipc/
│ │ │ ├── client.ts
│ │ │ ├── get-pipe-path.ts
│ │ │ └── server.ts
│ │ ├── is-windows.ts
│ │ ├── map-ts-extensions.ts
│ │ ├── node-features.ts
│ │ ├── path-utils.ts
│ │ ├── read-json-file.ts
│ │ ├── sha1.ts
│ │ ├── temporary-directory.ts
│ │ ├── transform/
│ │ │ ├── apply-transformers.ts
│ │ │ ├── cache.ts
│ │ │ ├── get-esbuild-options.ts
│ │ │ ├── index.ts
│ │ │ └── transform-dynamic-import.ts
│ │ ├── tsconfig.ts
│ │ └── url-search-params-stringify.ts
│ └── watch/
│ ├── index.ts
│ └── utils.ts
├── tests/
│ ├── fixtures/
│ │ └── test.wasm
│ ├── fixtures.ts
│ ├── index.ts
│ ├── specs/
│ │ ├── api.ts
│ │ ├── cli.ts
│ │ ├── loaders.ts
│ │ ├── repl.ts
│ │ ├── smoke.ts
│ │ ├── transform.ts
│ │ ├── tsconfig.ts
│ │ └── watch.ts
│ └── utils/
│ ├── coverage-sources-content.ts
│ ├── expect-match-in-order.ts
│ ├── get-node.ts
│ ├── is-process-alive.ts
│ ├── is-windows.ts
│ ├── node-versions.ts
│ ├── package-types.ts
│ ├── process-interact.ts
│ ├── pty-shell/
│ │ └── index.ts
│ └── tsx.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2
================================================
FILE: .github/DISCUSSION_TEMPLATE/q-a.yml
================================================
body:
- type: markdown
attributes:
value: |
## 💬 What's your question?
<details>
<summary><em>Need some pointers on crafting your question?</em></summary>
<br>
1. **Start succinctly:** Jump to the point with a concise question.
2. **Conciseness matters:** Keep it brief yet specific; avoid lengthy explanations.
3. **Include code:** Attach relevant code snippets, configurations, and errors.
4. **Add screenshots:** If applicable, provide screenshots.
</details>
- type: textarea
id: improvements
attributes:
label: Question
validations:
required: true
- type: markdown
attributes:
value: |
<br>
> **🚀 Try _Priority Support_!**
>
> If you're in a pinch, don't hesitate to take advantage of my [_Priority Support_ service](https://github.com/sponsors/privatenumber) where you can ask me questions in an exclusive forum. I'm well equppied to assist you with this project and would be happy to help you out! 🙂
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: 🐛 Bug report
description: Found a bug? File a report and let's get it fixed!
labels: [bug, pending triage]
body:
- type: markdown
attributes:
value: |
💁♂️ This is a collaborative effort. Please do your best to debug, communicate, and demonstrate the problem.
- type: checkboxes
attributes:
label: Acknowledgements
options:
- label: I read the documentation and searched existing issues to avoid duplicates
required: true
- label: I understand this is a **bug tracker** and anything other than a proven bug will be closed
required: true
- label: I understand this is a free project and relies on community contributions
required: true
- label: I read and understood the [Contribution guide](https://github.com/privatenumber/tsx/blob/master/CONTRIBUTING.md)
required: true
- type: markdown
attributes:
value: |
## 📋 Show us the bug
> [!CAUTION]
> Without proving the bug is in tsx via *minimal* reproduction, your issue will be closed without response.
The minimal reproduction is the core of your issue. This is usually all we read.
The first thing we'll do is scrutinize every code, file, and dependency to see how they impact the bug. If you can delete it and still produce the bug, it's not needed.
**Starter Template**: [Fork this StackBlitz template](https://stackblitz.com/edit/node-huzszn?file=index.ts)
If it's not reproducible on StackBlitz, upload it to a GitHub repository and use GitHub Actions to show the error.
Please do your best to _show_ the bug rather than talking about it. This will lead to a speedy resolution.
- type: input
attributes:
label: Minimal reproduction URL
placeholder: https://github.com/...
validations:
required: true
- type: markdown
attributes:
value: |
> [!TIP]
> **🙋 Need help?** Use [_Priority Support_](https://github.com/sponsors/privatenumber) for debugging help and implementation!
- type: markdown
attributes:
value: |
## 🗒️ Notes
> [!IMPORTANT]
> Please skip this section and focus on minimizing the reproduction. We only read it _after_ verifying the reproduction above.
- type: textarea
attributes:
label: Problem & expected behavior (under 200 words)
value: |
Skip this section and let your reproduction do the talking...
But if you really need to explain, keep it under 200 words:
- What happened
- What I expected
- Error stack trace
- Documentation links
- Screenshots
- type: markdown
attributes:
value: |
## 🛠️ Contribute
- type: checkboxes
attributes:
label: Bugs are expected to be fixed by those affected by it
options:
- label: I'm interested in working on this issue
- type: checkboxes
attributes:
label: Compensating engineering work will speed up resolution and support the project
options:
- label: I'm willing to offer $10 for financial support
- type: markdown
attributes:
value: |
## 🚀 Need immediate help?
Escalate this issue by supporting the project as a [_Priority Patron_](https://github.com/sponsors/privatenumber)!
Your concern will receive prompt attention, ensuring faster and more efficient resolution.
[→ Become a _Priority Patron_!](https://github.com/sponsors/privatenumber)
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 💖 [Sponsors only] Feature requests / Help / Questions
url: https://github.com/sponsors/privatenumber/
about: Need help ASAP? Get prioritized help for all your questions and issues!
- name: 📚 Contribution guide
url: https://github.com/privatenumber/tsx/blob/develop/CONTRIBUTING.md
about: Learn more about contributing to this project
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"enabledManagers": ["nvm"]
}
================================================
FILE: .github/workflows/lock-threads.yml
================================================
name: Lock threads
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 0" # Every Sunday
jobs:
lock:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
discussions: write
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: "91"
add-issue-labels: outdated
pr-inactive-days: "91"
add-pr-labels: outdated
discussion-inactive-days: "91"
add-discussion-labels: outdated
log-output: "true"
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
branches: [master, develop]
permissions:
contents: write
jobs:
release:
name: Release
if: (
github.repository_owner == 'pvtnbr' && github.ref_name =='develop'
) || (
github.repository_owner == 'privatenumber' && github.ref_name =='master'
)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: true
- name: Lint
run: pnpm lint
- name: Prerelease to GitHub
if: github.repository_owner == 'pvtnbr'
run: |
git remote add public https://github.com/$(echo $GITHUB_REPOSITORY | sed "s/^pvtnbr/privatenumber/")
git fetch public master 'refs/tags/*:refs/tags/*'
git push --force --tags origin refs/remotes/public/master:refs/heads/master
jq '
.publishConfig.registry = "https://npm.pkg.github.com"
| .name = ("@" + env.GITHUB_REPOSITORY_OWNER + "/" + .name)
| .repository = env.GITHUB_REPOSITORY
| .release.branches = [
"master",
{ name: "develop", prerelease: "rc", channel: "latest" }
]
' package.json > _package.json
mv _package.json package.json
- name: Release
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: pnpm dlx semantic-release@24
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
push:
branches: [master, develop]
pull_request:
jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Authenticate GitHub registry
run: npm set "//npm.pkg.github.com:_authToken=${{ secrets.GH_TOKEN }}"
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
shell: bash
run: echo "PNPM_STORE=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Restore pnpm cache
id: pnpm-cache
uses: actions/cache@v3
with:
path: ${{ env.PNPM_STORE }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-
- name: Install dependencies
run: pnpm install
- name: Save pnpm cache
if: steps.pnpm-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v3
with:
path: ${{ env.PNPM_STORE }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Get Node.js cache directory
shell: bash
run: echo "NVE_CACHE=$(node -p 'require("cachedir")("nve")')" >> $GITHUB_ENV
- name: Cache Node.js versions
uses: actions/cache@v3
with:
path: ${{ env.NVE_CACHE }}
key: ${{ runner.os }}-nodejs-${{ hashFiles('.nvmrc') }}-${{ hashFiles('tests/utils/node-versions.ts') }}
restore-keys: ${{ runner.os }}-nodejs-
- name: Test
run: pnpm test
- name: Type check
if: ${{ matrix.os == 'ubuntu-latest' }}
run: pnpm type-check
- name: Lint
if: ${{ matrix.os == 'ubuntu-latest' }}
run: pnpm lint
================================================
FILE: .gitignore
================================================
# macOS
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Dependency directories
node_modules
# Output of 'npm pack'
*.tgz
# dotenv environment variables file
.env
.env.test
# Cache
.eslintcache
# Distribution
dist
# Link config
link.config.json
# Vitepress
docs/.vitepress/cache
================================================
FILE: .nvmrc
================================================
20.19.3
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules/typescript/lib"
}
================================================
FILE: CONTRIBUTING.md
================================================
# Contribution guide
Welcome! We're excited you're interested in contributing. To ensure a smooth and productive collaboration, follow these guidelines.
## Goals of tsx
1. Enhance Node.js with TypeScript support
2. Improve ESM ↔ CommonJS interoperability as the ecosystem migrates to ESM
3. Support the [active LTS versions of Node.js](https://endoflife.date/nodejs)
## Issues & Discussions
> [!IMPORTANT]
> Please be polite, respectful, and considerate of people's time and effort.
>
> This is an open source project relying on community contributions.
### Opening a new Issue
> [!IMPORTANT]
> The Issues tab is a bug & feature tracker. Not a place for debugging support. To keep threads concise and easy to follow for collaborators, please participate constructively and follow the guidelines.
#### Minimal reproduction required
Provide a clear, minimal example of the issue. This helps contributors identify genuine bugs efficiently.
#### Check the documentation
Review the project documentation for known behaviors or caveats to avoid unnecessary issues.
### Commenting on an existing Issue
#### ⭐️ Issue objectives
Issues serve as a platform for **contributors** to:
1. Verify bugs
2. Diagnose the causes
3. Brainstorm solutions
4. Implement fix
#### ✅ Be constructive
Aim to contribute to the solution with research & PRs (tests + solutions). The goal is to land an improvement in _tsx_ for everyone to benefit.
Be concise to save people's time.
#### ❌ Avoid detractive comments
Keep comments constructive towards fixing the issue.
These types of comments will be hidden or deleted:
- Comments that pollute the thread (e.g. "updates?", "me too", "also happening in ...")
- Discussing workarounds that can't land in _tsx_
- Off-topic comments
After an issue is confirmed by the provided reproduction, the thread may sometimes be locked to direct further conversation & action to PRs.
> [!TIP]
> **⚡️ Get issues addressed faster!**
>
> Sponsors can prioritize issues. By helping fund development, you can ensure your needs are addressed quickly!
>
> [Sponsor now →](https://github.com/sponsors/privatenumber)
## Pull requests
#### Open an Issue first
Ensure there’s an existing issue related to your PR to facilitate alignment and prevent wasted work.
#### Include tests
Add minimal tests verifying your changes to maintain behavior and reliability.
## Development
### Initial setup
After cloning the repo, use [nvm](https://nvm.sh) (optional) to set the expected Node.js version, and [pnpm](https://pnpm.io) to install dependencies:
```bash
nvm i # Install or use Node.js version
pnpm i # Install dependencies
```
### Building
Build the source code with:
```bash
pnpm build # Compiles to `dist`
```
> [!TIP]
> Temporarily disable minification by removing `--minify` in `package.json#scripts.build` for easier debugging.
### Linting and type-checking
Ensure code quality with:
```bash
pnpm lint # ESLint
pnpm type-check # TypeScript type checking
```
### Testing
Run automated tests with:
```bash
pnpm test # Regular test
CI=1 pnpm test # CI environments
```
### Manual testing
#### Local testing
Use the absolute path to run `./dist/cli.mjs`:
```sh
/tsx/dist/cli.mjs <ts file>
```
#### Collaborative testing
Use [`git-publish`](https://github.com/privatenumber/git-publish) to publish your changes to your GitHub fork. It can be shared with others and installed from for testing.
## Giving back
<img align="center" src="https://badgen.net/npm/dm/tsx">
_tsx_ has outgrown its "hobby project" status to become a tool used and loved by many.
While it made it far without sponsorship, funding will accelerate further development by making it easier for devs to choose tsx over other paid work.
If tsx has helped you, help tsx back! ❤️
Any amount makes a difference.
[Sponsor now →](https://github.com/sponsors/privatenumber)
================================================
FILE: FUNDING.json
================================================
{
"drips": {
"ethereum": {
"ownedBy": "0xB9C2F7C3BB7734c37d2949B98A90bAea2fd59E21"
}
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<h1 align="center">
<br>
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/logo-dark.svg">
<img width="160" alt="tsx" src=".github/logo-light.svg">
</picture>
<br><br>
<a href="https://npm.im/tsx"><img src="https://badgen.net/npm/v/tsx"></a> <a href="https://npm.im/tsx"><img src="https://badgen.net/npm/dm/tsx"></a>
</h1>
<p align="center">
TypeScript Execute (tsx): The easiest way to run TypeScript in Node.js
<br><br>
<a href="https://tsx.is">Documentation</a> | <a href="https://tsx.is/getting-started">Getting started →</a>
</p>
<br>
<p align="center">
<a href="https://github.com/sponsors/privatenumber/sponsorships?tier_id=398771"><img width="412" src="https://raw.githubusercontent.com/privatenumber/sponsors/master/banners/assets/donate.webp"></a>
<a href="https://github.com/sponsors/privatenumber/sponsorships?tier_id=416984"><img width="412" src="https://raw.githubusercontent.com/privatenumber/sponsors/master/banners/assets/sponsor.webp"></a>
</p>
<p align="center"><sup><i>Already a sponsor?</i> Join the discussion in the <a href="https://github.com/pvtnbr/tsx">Development repo</a>!</sup></p>
## Sponsors
<p align="center">
<a href="https://github.com/sponsors/privatenumber">
<img src="https://cdn.jsdelivr.net/gh/privatenumber/sponsors/sponsorkit/sponsors.svg">
</a>
</p>
================================================
FILE: docs/.vitepress/config.ts
================================================
import { defineConfig } from 'vitepress';
const title = 'tsx';
const description = 'tsx (TypeScript Execute) - The easiest way to run TypeScript in Node.js';
export default defineConfig({
lang: 'en-US',
title,
description,
lastUpdated: true,
cleanUrls: true,
ignoreDeadLinks: true,
metaChunk: true,
head: [
['link', {
rel: 'icon',
type: 'image/svg+xml',
href: '/logo-mini.svg',
}],
['meta', {
property: 'og:title',
content: title,
}],
['meta', {
property: 'og:type',
content: 'website',
}],
['meta', {
property: 'og:image',
content: 'https://tsx.is/social.png',
}],
['meta', {
property: 'og:url',
content: 'https://tsx.is',
}],
['meta', {
property: 'og:description',
content: description,
}],
['meta', {
property: 'og:site_name',
content: title,
}],
['meta', {
name: 'twitter:card',
content: 'summary_large_image',
}],
['meta', {
name: 'twitter:site',
content: '@tsx_is',
}],
['script', {
src: 'https://beamanalytics.b-cdn.net/beam.min.js',
'data-token': 'ee893e1d-7484-4fb3-85b7-78c58b3d9c9e',
async: '',
}],
],
themeConfig: {
siteTitle: false,
logo: {
light: '/logo-light.svg',
dark: '/logo-dark.svg',
alt: 'tsx',
},
outline: 'deep',
editLink: {
pattern: 'https://github.com/privatenumber/tsx/edit/master/docs/:path',
text: 'Propose changes to this page',
},
nav: [
{
text: 'User guide',
link: '/',
activeMatch: '^(?!\/dev-api\/).*',
},
{
text: 'Developer API',
link: '/dev-api/',
activeMatch: '/dev-api/',
},
],
sidebar: {
'/': [
{
text: 'Introduction',
items: [
{
text: 'About tsx',
link: '/',
},
{
text: 'Getting started',
link: '/getting-started',
},
],
},
{
text: 'Usage',
items: [
{
text: 'Node.js enhancement',
link: '/node-enhancement',
},
{
text: 'Watch mode',
link: '/watch-mode',
},
],
},
{
text: 'Integration',
items: [
{
text: 'TypeScript',
link: '/typescript',
},
{
text: 'Compilation',
link: '/compilation',
},
{
text: 'Shell scripts',
link: '/shell-scripts',
},
{
text: 'VSCode debugging',
link: '/vscode',
},
]
},
{
text: 'Support',
items: [
{
text: 'FAQ',
link: '/faq',
docFooterText: 'Frequently Asked Questions',
},
{
text: 'Learning resources',
link: '/learn',
},
{
text: 'Help center (Sponsors only)',
link: 'https://github.com/pvtnbr/tsx/discussions',
},
{
text: 'Become a Sponsor',
link: 'https://github.com/sponsors/privatenumber/sponsorships?tier_id=416984',
},
],
},
],
'/dev-api/': [
{
text: 'About the Developer API',
link: '/dev-api/',
},
{
text: 'Automatic registration',
items: [
{
text: 'Node.js CLI',
link: '/dev-api/node-cli'
},
{
text: 'Entry-point',
link: '/dev-api/entry-point'
},
],
},
{
text: 'Scoped TS loading',
items: [
{
text: 'tsImport()',
link: '/dev-api/ts-import'
},
{
text: 'tsx.require()',
link: '/dev-api/tsx-require'
},
],
},
{
text: 'Register API',
items: [
{
text: 'CommonJS',
link: '/dev-api/register-cjs'
},
{
text: 'ESM',
link: '/dev-api/register-esm'
},
],
},
],
},
socialLinks: [
{
icon: 'github',
link: 'https://github.com/privatenumber/tsx',
},
{
icon: 'x',
link: 'https://x.com/tsx_is',
},
{
icon: {
svg: '<svg viewBox="0 0 24 24"><path d="M22 4H2v16h20zm-2 4l-8 5l-8-5V6l8 5l8-5z"/></svg>'
},
link: '/contact'
}
],
search: {
provider: 'local',
},
carbonAds: {
code: 'CW7DEKJY',
placement: 'tsxis',
},
},
});
================================================
FILE: docs/.vitepress/theme/components/AsideSponsors.vue
================================================
<template>
<div class="mb-8">
<div>
<h4 class="text-base font-semibold">Premium sponsors</h4>
</div>
<a
href="https://sevalla.com"
target="_blank"
rel="noopener"
>
<div class="my-4 py-6 px-6 bg-white/5">
<img
src="/logos/sevalla-full.svg"
alt="Sevalla"
class="object-cover"
>
</div>
</a>
<a
href="https://github.com/sponsors/privatenumber/sponsorships?pay_prorated=false&tier_id=388346"
target="_blank"
rel="noopener"
class="sponsor-button"
>
Become a premium sponsor
</a>
</div>
</template>
<style scoped>
.sponsor-button {
@apply
block
w-fit
mx-auto
text-xs
rounded-full
transition-colors
bg-pink-500
hover:bg-pink-600
text-white
mt-5
py-1.5
px-4;
}
</style>
================================================
FILE: docs/.vitepress/theme/components/ContactForm.vue
================================================
<script setup lang="ts">
import { ref } from 'vue';
const isSending = ref(false);
const name = ref('');
const email = ref('');
const subject = ref('');
const message = ref('');
const sendMessage = async () => {
isSending.value = true;
const body = new FormData();
body.append('name', name.value);
body.append('email', email.value);
body.append('subject', subject.value);
body.append('message', message.value);
body.append('access_key', '122038f1-9646-4bb3-89de-55ab3e1bed50');
const fetching = await fetch(
'https://api.web3forms.com/submit',
{
method: 'POST',
body,
},
);
const response = await fetching.json();
if (response.success) {
// show popup
}
name.value = '';
email.value = '';
subject.value = '';
message.value = '';
isSending.value = false;
};
</script>
<template>
<div class="mx-auto">
<form @submit.prevent="sendMessage">
<div
class="
flex
flex-wrap
md:flex-nowrap
gap-4
my-4
"
>
<label class="w-full">
<div class="label">Name</div>
<input
v-model="name"
class="w-full"
type="text"
name="name"
required
:disabled="isSending"
placeholder="Your name"
>
</label>
<label class="w-full">
<div class="label">Email</div>
<input
v-model="email"
class="w-full"
type="email"
name="email"
required
:disabled="isSending"
placeholder="your@email.com"
>
</label>
</div>
<div
class="
flex
flex-wrap
md:flex-nowrap
gap-4
my-4
"
>
<label class="w-full">
<div class="label">Subject</div>
<input
v-model="subject"
class="w-full"
type="text"
name="subject"
required
:disabled="isSending"
placeholder="Sponsorship, consultation, etc."
>
</label>
</div>
<label class="block my-5">
<div class="label">Message</div>
<textarea
v-model="message"
class="w-full h-40"
placeholder="Hey there, can I ask you a question?"
:disabled="isSending"
required
/>
</label>
<div class="flex flex-row-reverse">
<button
title="Send email"
type="submit"
class="send-email button"
:disabled="isSending"
>
<template v-if="isSending">
Sending...
</template>
<template v-else>
Send
</template>
</button>
</div>
</form>
</div>
</template>
<style scoped>
input, textarea, select {
@apply
rounded
bg-zinc-100
dark:bg-zinc-950
py-2
px-4;
}
label {
@apply
flex
flex-col
gap-2;
}
.send-email {
@apply
bg-blue-500
text-white;
}
</style>
================================================
FILE: docs/.vitepress/theme/components/ImageLink.vue
================================================
<script setup lang="ts">
defineProps<{
imgSrc: string;
href: string;
alt: string;
}>();
</script>
<template>
<a
class="link"
:href="href"
:title="alt"
>
<img
:src="imgSrc"
:alt="alt"
>
</a>
</template>
<style scoped>
.link {
@apply
p-2
box-border;
}
img {
@apply
h-full
drop-shadow;
}
</style>
================================================
FILE: docs/.vitepress/theme/components/Marquee.vue
================================================
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const reflow = (element: HTMLElement) => {
element.style.display = 'none';
element.offsetHeight;
element.style.display = '';
};
const waitImageLoad = (
img: HTMLImageElement,
) => new Promise<void>(
(resolve) => {
if (img.complete) {
return resolve();
}
img.addEventListener('load', () => resolve());
img.addEventListener('error', () => resolve());
}
);
const waitImages = (
element: HTMLElement,
) => {
const images = element.querySelectorAll('img');
return Promise.all(
Array.from(images).map(async (image) => {
await waitImageLoad(image);
/**
* Bug in Safari where preloaded image causes the image to have
* 0 width.
* Load the FAQ page then navigating to the index page.
* Solution is to trigger reflow.
*/
if (image.offsetWidth === 0) {
reflow(image);
}
})
);
};
const props = defineProps<{
velocity: number;
}>();
const $content = ref(null);
/**
* Animation shouldn't start until the content is loaded and the width is
* determined. Otherwise it will move at the rate of the initial width
*/
const animationDuration = ref({
animationDuration: '',
});
const computeAnimationDuration = () => {
animationDuration.value.animationDuration = `${$content.value.offsetWidth / props.velocity}s`;
};
const isPageVisible = ref<boolean>();
const checkIsPageVisible = () => {
isPageVisible.value = document.visibilityState === 'visible';
};
onMounted(() => {
document.addEventListener('visibilitychange', checkIsPageVisible);
checkIsPageVisible();
if ($content.value) {
waitImages($content.value).then(computeAnimationDuration);
}
});
</script>
<template>
<div
:class="[
'marquee',
{ moving: isPageVisible },
]"
>
<div
class="content"
ref="$content"
:style="animationDuration"
>
<slot />
</div>
<div
class="content"
:style="animationDuration"
>
<slot />
</div>
</div>
</template>
<style scoped>
.marquee {
display: flex;
white-space: nowrap;
overflow: hidden;
}
.content {
min-width: max-content;
}
.moving .content {
animation: move-left linear infinite;
}
@keyframes move-left {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
</style>
================================================
FILE: docs/.vitepress/theme/components/Sponsors.vue
================================================
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const svgUrl = 'https://cdn.jsdelivr.net/gh/privatenumber/sponsors/sponsorkit/sponsors.svg';
const svgContent = ref<string>('');
const fetchSvg = async () => {
const response = await fetch(svgUrl);
const svgText = await response.text();
svgContent.value = svgText.replace('<svg ', '<svg style="width:100%;height:auto;" ');
};
onMounted(() => {
fetchSvg();
});
</script>
<template>
<div
v-if="svgContent"
v-html="svgContent"
/>
<a
v-else
href="https://github.com/sponsors/privatenumber/sponsorships?tier_id=416984"
target="_blank"
>
<img :src="svgUrl">
</a>
</template>
================================================
FILE: docs/.vitepress/theme/index.ts
================================================
import { h } from 'vue';
import DefaultTheme from 'vitepress/theme';
import AsideSponsors from './components/AsideSponsors.vue';
import './styles.css';
export default {
extends: DefaultTheme,
Layout: () => h(
DefaultTheme.Layout,
null,
{
'aside-ads-before': () => h(AsideSponsors),
},
),
};
================================================
FILE: docs/.vitepress/theme/styles.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
.button {
@apply
inline-block
py-2
px-6
rounded-full
!no-underline
!transition-colors
duration-200
;
}
.dark .dark-zebra-pattern {
background-image: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgb(0 0 0/ 0.05) 10px,
rgb(0 0 0/ 0.05) 20px
);
}
================================================
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](https://x.com/atcb/status/1705675335814271157).
:::
## Compiling an npm package
npm packages are distinguished from applications by defining package entry-points in `package.json`.
[pkgroll](https://github.com/privatenumber/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`](https://nodejs.org/api/packages.html#exports):
```json5
// package.json
{
"exports": "./dist/index.mjs"
}
```
:::
::: details Dual package (CJS & ESM)
Set your CommonJS & Module entry-points in [`exports`](https://nodejs.org/api/packages.html#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`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#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 ++]
}
```
<!-- ## Compiling an application -->
================================================
FILE: docs/contact.md
================================================
# Contact
<script setup lang="ts">
import ContactForm from './.vitepress/theme/components/ContactForm.vue';
</script>
<ContactForm />
================================================
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')
```
<!-- TODO: does this work in CJS mode? -->
Note, because of the order of static import evaluation in ESM, the enhancement only works on [dynamic imports after registration](https://nodejs.org/docs/latest-v22.x/api/module.html#:~:text=dynamic%20import()%20must%20be%20used%20for%20any%20code%20that%20should%20be%20run%20after%20the%20hooks%20are%20registered).
::: 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](/dev-api/node-cli).
:::
## 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_](/watch-mode.md) and the [REPL](/usage#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`](/dev-api/node-cli) or you can do [`import 'tsx'`](/dev-api/entry-point) 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](/dev-api/ts-import) 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`](https://nodejs.org/api/packages.html#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](https://nodejs.org/api/cli.html#node_optionsoptions) 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`](/dev-api/register-esm).
- 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 tsx = require('tsx/cjs/api')
const api = tsx.register({
// Pass in a unique namespace
namespace: Date.now().toString()
})
// 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 { register } from 'tsx/esm/api'
const api = register({
// Pass in a unique namespace
namespace: Date.now().toString()
})
// 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](https://v8.dev/features/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](/dev-api/register-esm.md) 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()`](/dev-api/ts-import) 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/faq.md
================================================
<script setup lang="ts">
import ImageLink from './.vitepress/theme/components/ImageLink.vue';
</script>
# Frequently Asked Questions
## How can I do ______ in _tsx_?
We understand that searching for answers about _tsx_ can be challenging due to its name. However, since _tsx_ is essentially an alias for `node` and adheres to TypeScript behavior, asking how to do something in _tsx_ might not be the best approach.
Instead, consider these questions:
- _"How can I do ______ in Node.js?"_
- _"How can I do ______ in TypeScript?"_
If your question specifically relates to _tsx_, you can use the search feature in the documentation or [ask a question](#ask-a-question).
## I found a bug in _tsx_. What should I do?
If you've confirmed that the bug is in _tsx_, please file an issue in the _tsx_ GitHub repository. Make sure to include a minimal reproduction of the bug.
<a class="button bug-report" href="https://github.com/privatenumber/tsx/issues/new?assignees=&labels=bug%2Cpending+triage&projects=&template=bug-report.yml" target="_blank">
File a bug report ↗
</a>
## Who uses _tsx_?
_tsx_ is recognized by both [Node.js](https://nodejs.org/en/learn/getting-started/nodejs-with-typescript#running-typescript-code-with-tsx) and [TypeScript](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html#im-using-tsx) as a popular tool for running TypeScript code. It is widely adopted, as indicated by its npm download stats: <a href="https://npm.im/tsx"><img class="inline-block" src="https://badgen.net/npm/dm/tsx"></a>.
### Companies
<div class="logos dark-zebra-pattern">
<ImageLink
alt="Vercel"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Avercel&type=code"
img-src="/logos/vercel.svg"
/>
<ImageLink
alt="Google"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Agoogle&type=code"
img-src="/logos/google.svg"
/>
<ImageLink
alt="GitHub"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Agithub&type=code"
img-src="/logos/github.svg"
/>
<ImageLink
alt="Square"
href="https://github.com/square"
img-src="/logos/square.svg"
/>
<ImageLink
alt="Supabase"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Asupabase&type=code"
img-src="/logos/supabase.svg"
/>
<ImageLink
alt="Astro"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Awithastro&type=code"
img-src="/logos/astro.svg"
/>
<ImageLink
alt="OpenAI"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aopenai&type=code"
img-src="/logos/openai.svg"
/>
<ImageLink
alt="IBM"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aibm&type=code"
img-src="/logos/ibm.svg"
/>
<ImageLink
alt="Codecov"
href="https://github.com/codecov"
img-src="/logos/codecov.svg"
/>
<ImageLink
alt="Amazon AWS"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aaws+OR+org%3Aawslabs&type=code"
img-src="/logos/aws.svg"
/>
<ImageLink
alt="Sentry"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Agetsentry&type=code"
img-src="/logos/sentry.svg"
/>
<ImageLink
alt="Microsoft"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Amicrosoft&type=code"
img-src="/logos/microsoft.svg"
/>
<ImageLink
alt="Meta"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Afacebook&type=code"
img-src="/logos/meta.svg"
/>
<ImageLink
alt="Alibaba"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aalibaba&type=code"
img-src="/logos/alibaba.svg"
/>
<ImageLink
alt="Mozilla"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Amozilla&type=code"
img-src="/logos/mozilla.svg"
/>
<ImageLink
alt="Figma"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Afigma&type=code"
img-src="/logos/figma.svg"
/>
<ImageLink
alt="Cloudflare"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Acloudflare&type=code"
img-src="/logos/cloudflare.svg"
/>
<ImageLink
alt="Salesforce"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Asalesforce&type=code"
img-src="/logos/salesforce.svg"
/>
</div>
### Projects
<div class="logos dark-zebra-pattern">
<ImageLink
alt="Node.js"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Anodejs&type=code"
img-src="/logos/nodejs.svg"
/>
<ImageLink
alt="Electron"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aelectron&type=code"
img-src="/logos/electron.svg"
/>
<ImageLink
alt="Vite"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Avitejs&type=code"
img-src="/logos/vite.svg"
/>
<ImageLink
alt="Mermaid"
href="https://github.com/mermaid-js/mermaid/blob/3809732e48a0822fad596d0815a6dc0e166dda94/package.json#L121"
img-src="/logos/mermaid.svg"
/>
<ImageLink
alt="Vue.js"
href="https://github.com/vuejs/core/blob/70641fc0deb857464d24aa7ab7eaa18e2a855146/package.json#L110"
img-src="/logos/vue.svg"
/>
<ImageLink
alt="11ty"
href="https://www.11ty.dev/docs/languages/typescript/"
img-src="/logos/11ty.svg"
/>
<ImageLink
alt="Vitest"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Avitest-dev&type=code"
img-src="/logos/vitest.svg"
/>
<ImageLink
alt="date-fns"
href="https://github.com/date-fns/date-fns/blob/5c1adb5369805ff552737bf8017dbe07f559b0c6/package.json#L6123"
img-src="/logos/date-fns.svg"
/>
<ImageLink
alt="Cheerio"
href="https://github.com/cheeriojs/cheerio/blob/d0b3c2f6b57cd1f835741175d463963266be0eef/package.json#L99"
img-src="/logos/cheerio.svg"
/>
<ImageLink
alt="Zod"
href="https://github.com/colinhacks/zod/blob/7173d0bcc2105777102e22d36a2866196e2830f3/package.json#L45"
img-src="/logos/zod.svg"
/>
<ImageLink
alt="WebDriverIO"
href="https://github.com/webdriverio/webdriverio/blob/5955cda26a538a12b10d686a394beb1b109fe49d/package.json#L122"
img-src="/logos/webdriverio.svg"
/>
<ImageLink
alt="Knip"
href="https://github.com/webpro-nl/knip/blob/b9b8f4da3dcbff1af309bfbb7890bb9ec36124f9/packages/knip/package.json#L48"
img-src="/logos/knip.svg"
/>
<ImageLink
alt="ArkType"
href="https://github.com/arktypeio/arktype/blob/1820e86e93e1ccaabda7f8de28fb7f04320d6fc8/package.json#L59"
img-src="/logos/arktype.svg"
/>
<ImageLink
alt="Prisma"
href="https://github.com/prisma/prisma/blob/f19a3e28a37327eb0a5e45941c0e4abf3dbab136/package.json#L94"
img-src="/logos/prisma.svg"
/>
</div>
To find more examples of where & how _tsx_ is used, [search on GitHub](https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22&type=code).
## How does _tsx_ compare to [`ts-node`](https://github.com/TypeStrong/ts-node)?
`tsx` and `ts-node` are both tools for running TypeScript in Node.js, each offering different approaches to suit user preferences.
**Installation**
- **tsx**: Can be used without installation (via `npx tsx ./script.ts`) and comes as a single binary with no peer dependencies.
- **ts-node**: Requires installation of TypeScript or SWC as peer dependencies.
**Configuration**
- **tsx**: Works out of the box without needing a `tsconfig.json` file, making it easy for beginners.
- **ts-node**: May require initial setup and configuration.
**Defaults**
- **tsx**: Uses sensible defaults based on file imports and Node.js version, reducing the need for certain `tsconfig.json` settings.
- **ts-node**: Relies on TypeScript's defaults, which might need adjusting.
**Module Support**
- **tsx**: Automatically adapts between CommonJS and ESM modules, supporting `require()` for ESM modules.
- **ts-node**: Provides module support but may need configuration for certain scenarios.
**Syntax and Features**
- **tsx**: Supports new JS & TS syntax and features based on Node.js version and includes support for `tsconfig.json` paths.
- **ts-node**: Uses the TypeScript compiler and may require additional settings for certain features.
**Speed**
- **tsx**: Uses [esbuild](https://esbuild.github.io/faq/#:~:text=typescript%20benchmark) for fast compilation and does not perform type checking.
- **ts-node**: Uses the TypeScript compiler by default, with an option to use the SWC compiler for faster performance.
**Watcher**
As a DX bonus, _tsx_ also comes with [Watch mode](/watch-mode.md) to help you iterate faster!
For a detailed technical comparison, refer to this [exhaustive comparison](https://github.com/privatenumber/ts-runtime-comparison) between `tsx`, `ts-node`, and other runtimes.
## Can/should _tsx_ be used in production?
Deciding whether to use _tsx_ in production depends on your specific needs and risk tolerance. Here are a few points to consider:
- _tsx_ is simply a Node.js enhancement, so you can generally expect a similar level of stability.
- _tsx_ uses [esbuild](https://esbuild.github.io) for transforming TypeScript and ESM. Although esbuild is already adopted in many production ready tools, keep in mind that it technically hasn't reached a stable release yet.
Here are some questions to help guide your decision:
- What are the benefits versus the costs of using _tsx_ in production? Are there performance improvements?
- Does _tsx_ run your code as expected? Are there any differences between development and production environments?
- Can you rely on this open-source project and its maintainers? Consider [sponsoring](https://github.com/sponsors/privatenumber/sponsorships?tier_id=416984) the project to support its growth and stability.
Ultimately, it's a decision you'll need to make based on your specific production requirements and comfort level with potential risks.
---
# Ask a question
_tsx_ offers support via [Discussions](https://github.com/pvtnbr/tsx/discussions) for [sponsors](https://github.com/sponsors/privatenumber/sponsorships?tier_id=416984). If you're a sponsor, feel free to ask more questions there!
<style scoped>
.bug-report {
@apply
text-sm
text-white
hover:text-white
bg-blue-500
hover:bg-blue-600
;
}
.logos {
@apply
flex
flex-wrap
gap-x-4
gap-y-6
justify-around
my-4
py-6
px-4
dark:bg-zinc-800;
& :deep(img) {
@apply h-10;
}
}
</style>
================================================
FILE: docs/getting-started.md
================================================
# Getting started
### Prerequisites
Before you can start using _tsx_, ensure that you have [Node.js installed](https://nodejs.org/en/download/). _tsx_ is designed to be compatible with all [maintained versions](https://endoflife.date/nodejs) of Node.js.
## Quickstart
`tsx` can be executed with [npx](https://docs.npmjs.com/cli/commands/npx/)—a tool to run npm packages without installing them.
In your command-line, simply pass in a TypeScript file you'd like to run. It's that simple!
```sh
npx tsx ./script.ts
```
## Project installation
To install `tsx` as a project development dependency, run the following command in your project directory:
::: code-group
```sh [npm]
$ npm install -D tsx
```
```sh [pnpm]
$ pnpm add -D tsx
```
```sh [yarn]
$ yarn add -D tsx
```
:::
#### Using `tsx`
Once installed, you can invoke it with your package manager while in the project directory:
::: code-group
```sh [npm]
$ npx tsx ./file.ts
```
```sh [pnpm]
$ pnpm tsx ./file.ts
```
```sh [yarn]
$ yarn tsx ./file.ts
```
:::
#### Using it in `package.json#scripts`
Project commands are usually organized in the [`package.json#scripts`](https://docs.npmjs.com/cli/v10/using-npm/scripts) object.
In the `scripts` object, you can reference `tsx` directly without `npx`:
```js
// package.json
{
"scripts": {
"start": "tsx ./file.ts"// [!code highlight]
}
}
```
## Global installation
If you want to use `tsx` anywhere on your computer (without [`npx`](https://docs.npmjs.com/cli/commands/npx/)), install it globally:
::: code-group
```sh [npm]
$ npm install -g tsx
```
```sh [pnpm]
$ pnpm add -g tsx
```
```sh [yarn]
Yarn 2 doesn't support global installation
https://yarnpkg.com/migration/guide#use-yarn-dlx-instead-of-yarn-global
```
:::
This allows you to call `tsx` directly:
```sh
tsx file.ts
```
================================================
FILE: docs/index.md
================================================
---
outline: false
---
<div class="mb-10">
<img src="/logo-dark.svg" width="150" class="light:hidden" alt="tsx: TypeScript Execute">
<img src="/logo-light.svg" width="150" class="dark:hidden" alt="tsx: TypeScript Execute">
</div>
# TypeScript Execute <span class="font-normal">(_tsx_)</span>
_tsx_ stands for _TypeScript Execute_ and it's a Node.js enhancement to run [TypeScript](https://www.typescriptlang.org).
For starters, think of `tsx` as an alias to `node` and use it the same way:
<div class="tsx-before-after">
```sh
node file.js
```
<span class="hidden sm:block">→</span>
<span class="sm:hidden">↓</span>
```sh
tsx file.ts
```
</div>
You can pass in Node CLI flags and JS files too:
```sh
tsx --env-file=.env ./file.js
```
## Features
### Seamless TypeScript execution
Run TypeScript code without worrying about configuration!
_tsx_ runs your TypeScript code with modern and sensible defaults, making it user-friendly and especially great for beginners.
### Seamless CJS ↔ ESM imports
No need to wonder whether a package is CommonJS or ESM again.
If you've encountered the `ERR_REQUIRE_ESM` error in Node, you'll never see it again!
### Watch mode
As a DX bonus, _tsx_ comes with [Watch mode](/watch-mode.md) to re-run your files whenever you save them.
Iterate on your code faster and boost productivity!
---
<a href="/faq#who-uses-tsx" class="!no-underline">Who uses _tsx_?</a>
<Marquee class="mt-6 dark:bg-zinc-800 dark-zebra-pattern py-6" :velocity="20">
<div class="flex gap-6 items-center min-w-full">
<ImageLink
class="h-12"
alt="Vercel"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Avercel&type=code"
img-src="/logos/vercel.svg"
/>
<ImageLink
class="h-12"
alt="Google"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Agoogle&type=code"
img-src="/logos/google.svg"
/>
<ImageLink
class="h-12"
alt="GitHub"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Agithub&type=code"
img-src="/logos/github.svg"
/>
<ImageLink
class="h-12"
alt="Figma"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Afigma&type=code"
img-src="/logos/figma.svg"
/>
<ImageLink
class="h-12"
alt="Square"
href="https://github.com/square"
img-src="/logos/square.svg"
/>
<ImageLink
class="h-12"
alt="Microsoft"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Amicrosoft&type=code"
img-src="/logos/microsoft.svg"
/>
<ImageLink
class="h-12"
alt="OpenAI"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aopenai&type=code"
img-src="/logos/openai.svg"
/>
<ImageLink
class="h-12"
alt="Amazon AWS"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aaws+OR+org%3Aawslabs&type=code"
img-src="/logos/aws.svg"
/>
<ImageLink
class="h-12"
alt="Meta"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Afacebook&type=code"
img-src="/logos/meta.svg"
/>
<ImageLink
class="h-12"
alt="IBM"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aibm&type=code"
img-src="/logos/ibm.svg"
/>
<ImageLink
class="h-12"
alt="Alibaba"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Aalibaba&type=code"
img-src="/logos/alibaba.svg"
/>
<ImageLink
class="h-12"
alt="Mozilla"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Amozilla&type=code"
img-src="/logos/mozilla.svg"
/>
<ImageLink
class="h-12"
alt="Cloudflare"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Acloudflare&type=code"
img-src="/logos/cloudflare.svg"
/>
<ImageLink
class="h-12"
alt="Salesforce"
href="https://github.com/search?q=path%3Apackage.json+%22%5C%22tsx%5C%22%3A+%5C%22%22+org%3Asalesforce&type=code"
img-src="/logos/salesforce.svg"
/>
</div>
</Marquee>
## About the project
The idea for _tsx_ came about during a time when the Node.js ecosystem was getting fragmented due to the release of [ES Modules (ESM)](https://nodejs.org/api/esm.html). As packages migrated to ESM, projects struggled to reconcile their CommonJS apps with ESM dependencies.
Back then, _ts-node_ was the go-to tool for running TypeScript in Node.js, but it lacked ESM support and was quite complicated to use. We noticed several open-source tools using esbuild to run TypeScript in Node.js and decided to bring these efforts together into one simple, cohesive project.
**_tsx_ is designed to simplify your TypeScript experience.** It enhances Node.js with TypeScript support in both CommonJS and ESM modes, allowing you to switch between them seamlessly. It also supports `tsconfig.json` paths and includes a Watch mode to make development even easier.
Right now, the _tsx_ project development relies on user donations, which isn't sustainable in the long run. To keep _tsx_ reliable and growing, we need funding to cover maintenance and development costs.
If your company uses _tsx_ and would like to support the project, consider [sponsoring us](https://github.com/sponsors/privatenumber/sponsorships?pay_prorated=false&tier_id=388346)—in return we'll put up your logo + link!
## Sponsors
<p align="center">
<Sponsors />
<a class="button sponsor-button mt-10 mx-auto" href="https://github.com/sponsors/privatenumber/sponsorships?tier_id=416984" target="_blank">
Become a sponsor
</a>
</p>
<style scoped>
.tsx-before-after {
@apply
flex
justify-between
gap-4
items-center
flex-wrap
sm:flex-nowrap;
> * {
@apply
w-full
text-center
m-0;
}
> p {
@apply sm:w-auto;
}
}
.sponsor-button {
@apply
text-white
hover:text-white
bg-pink-500
hover:bg-pink-600
;
}
</style>
<script setup lang="ts">
import ImageLink from './.vitepress/theme/components/ImageLink.vue';
import Marquee from './.vitepress/theme/components/Marquee.vue';
import Sponsors from './.vitepress/theme/components/Sponsors.vue';
</script>
================================================
FILE: docs/learn.md
================================================
# Learning resources
TypeScript can be difficult to get started with and _tsx_ is all about lowering that barrier of entry.
Here are some great resources for your reference.
::: info
This page is currently a work in progress.
If you're a TypeScript educator or you know any TS courses that use tsx, [let us know](/contact)!
:::
## Documentation
- [TypeScript documentation](https://www.typescriptlang.org/docs/)
- [Node.js documentation](https://nodejs.org/docs/latest/api/)
================================================
FILE: docs/node-enhancement.md
================================================
# Node.js enhancement
## Swap `node` for `tsx`
`tsx` is a drop-in replacement for `node`, meaning you can use it the exact same way (supports all [command-line flags](https://nodejs.org/api/cli.html)).
If you have an existing `node` command, simply substitute it with `tsx`.
```sh
node --no-warnings --env-file=.env ./file.js
```
<p class="text-center">↓</p>
```sh
tsx --no-warnings --env-file=.env ./file.js
```
::: warning Node.js version matters
Under the hood, `tsx` calls `node`. This means the Node.js features supported in `tsx` depend on the Node.js version you have installed.
:::
## Flag & arguments positioning
Just like with `node`, correctly positioning flags and arguments is important when using `tsx`.
Place _tsx_ flags immediately after `tsx`, and place flags and arguments for your script after the script path.
```sh
tsx [tsx flags] ./file.ts [flags & arguments for file.ts]
```
## TypeScript REPL
_tsx_ extends the Node.js REPL to support TypeScript, allowing interactive coding sessions directly in TypeScript.
```sh
tsx
```
::: info What is the Node.js REPL?
The [Node.js REPL](https://nodejs.org/en/learn/command-line/how-to-use-the-nodejs-repl) is an interactive prompt that immediately executes input code, ideal for learning and experimenting. _tsx_ enhances this tool by adding TypeScript support.
:::
## Test runner
_tsx_ enhances the Node.js built-in [test runner](https://nodejs.org/api/test.html) with TypeScript support. You can use it the same way:
```sh
tsx --test
```
It will automatically recognize test files with TypeScript extensions:
- `**/*.test.?[cm][jt]s`
- `**/*-test.?[cm][jt]s`
- `**/*_test.?[cm][jt]s`
- `**/test-*.?[cm][jt]s`
- `**/test.?[cm][jt]s`
- `**/test/**/*.?[cm][jt]s`
================================================
FILE: docs/package.json
================================================
{
"name": "docs",
"private": true,
"type": "module",
"scripts": {
"dev": "vitepress dev --open",
"build": "vitepress build",
"prepare": "node ./scripts/hash-class-names.js ./node_modules/vitepress/dist/client/theme-default",
"preview": "vitepress preview"
},
"devDependencies": {
"autoprefixer": "^10.4.21",
"tailwindcss": "^3.4.17",
"vitepress": "^1.6.4"
}
}
================================================
FILE: docs/postcss.config.js
================================================
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: docs/scripts/hash-class-names.js
================================================
import fs from 'node:fs/promises';
import path from 'node:path';
const TARGET_CLASSES = ['VPDocAside'];
const ALLOWED_EXTENSIONS = new Set(['.css', '.js', '.vue']);
const generateHash = () => Math.random().toString(36).slice(2, 8);
const listFilesRecursive = async (directory) => {
const entries = await fs.readdir(directory, { withFileTypes: true });
const paths = await Promise.all(
entries.map(async (entry) => {
const resolved = path.resolve(directory, entry.name);
return entry.isDirectory() ? listFilesRecursive(resolved) : resolved;
}),
);
return paths.flat().filter(file => ALLOWED_EXTENSIONS.has(path.extname(file)));
};
const rewriteFiles = async (filePaths, classNames) => {
const classMap = new Map();
classNames.sort((a, b) => b.length - a.length).forEach((name) => {
classMap.set(name, `${name}-${generateHash()}`);
});
await Promise.all(
filePaths.map(async (filePath) => {
let content = await fs.readFile(filePath, 'utf8');
const original = content;
classMap.forEach((replacement, originalName) => {
const selectorPattern = new RegExp(`(?<![-_\\w])\\.${originalName}(?![-_\\w])`, 'g');
content = content.replace(selectorPattern, `.${replacement}`);
});
const classAttributePattern = /(class(?:Name)?\s*=\s*)(["'])(.*?)\2/g;
content = content.replaceAll(classAttributePattern, (_whole, attribute, quote, value) => {
const updated = String(value).split(/\s+/).map(cls => classMap.get(cls) || cls).join(' ');
return `${attribute}${quote}${updated}${quote}`;
});
if (content !== original) { await fs.writeFile(filePath, content, 'utf8'); }
}),
);
};
const input = process.argv[2];
if (!input) { throw new Error('Input directory is required.'); }
const directory = path.resolve(input);
const stats = await fs.stat(directory).catch(() => null);
if (!stats?.isDirectory()) { throw new Error('Input must be a valid directory.'); }
const filePaths = await listFilesRecursive(directory);
await rewriteFiles(filePaths, TARGET_CLASSES);
================================================
FILE: docs/shell-scripts.md
================================================
# Shell scripts
You can write a shell script in TypeScript by specifying _tsx_ in the [hashbang](https://bash.cyberciti.biz/guide/Shebang). The hashbang tells the shell how to run the script, making it executable.
### Project scripts
If `tsx` is installed in your project, use your package manager to reference _tsx_:
::: code-group
```ts [npm]
#!/usr/bin/env -S npx tsx
console.log('argv:', process.argv.slice(2))
```
```ts [pnpm]
#!/usr/bin/env -S pnpm tsx
console.log('argv:', process.argv.slice(2))
```
```ts [yarn]
#!/usr/bin/env -S yarn tsx
console.log('argv:', process.argv.slice(2))
```
:::
Make the file executable:
```sh
chmod +x ./file.ts
```
Now, you can run `./file.ts` directly:
```sh
./file.ts hello world
# Output: argv: [ 'hello', 'world' ]
```
### Global scripts
If `tsx` is installed globally, you can reference `tsx` directly in the hashbang:
_file.ts_:
```ts
#!/usr/bin/env tsx
console.log('argv:', process.argv.slice(2))
```
Don't forget to `chmod` the file to make it executable!
================================================
FILE: docs/tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
export default {
content: [
'**/*.md',
'.vitepress/**/*.vue',
],
darkMode: 'selector',
plugins: [
({ addVariant }) => addVariant('light', 'html:not(.dark) &'),
],
};
================================================
FILE: docs/typescript.md
================================================
# TypeScript
_tsx_ does not type check your code on its own and expects it to be handled separately. While _tsx_ doesn’t require TypeScript to be installed, and the type checks provided by your IDE might suffice for quick scripts, it is highly recommended to include a type checking step in your projects.
## Development workflow
Type checking is important but it can be time-consuming and expensive to do on every run.
`tsx` alleviates this problem by allowing you to execute TypeScript code directly without being blocked by type errors. Modern IDEs like VSCode provide real-time type checking via [IntelliSense](https://code.visualstudio.com/docs/languages/typescript), reducing the need for manual type checks. This workflow lets you iterate faster on functionality and treat type errors as linting errors rather than compilation requirements.
To incorporate type checking, include it with other linters (e.g. ESLint) in pre-commit hooks or CI checks.
## Installation
Start by installing the following in your project:
- [`typescript`](https://npmjs.com/package/typescript) to type-check with the `tsc` CLI command
- [`@types/node`](https://npmjs.com/package/@types/node) to provide TypeScript with Node.js API types
::: code-group
```sh [npm]
$ npm install -D typescript @types/node
```
```sh [pnpm]
$ pnpm add -D typescript @types/node
```
```sh [yarn]
$ yarn add -D typescript @types/node
```
:::
## tsconfig.json
[`tsconfig.json`](https://www.typescriptlang.org/tsconfig/) is the configuration file used by TypeScript.
### Recommendation
Here's the recommended configuration to make type-checking behave consistently.
```jsonc
{
"compilerOptions": {
// Treat files as modules even if it doesn't use import/export
"moduleDetection": "force",
// Ignore module structure
"module": "Preserve",
// Allow JSON modules to be imported
"resolveJsonModule": true,
// Allow JS files to be imported from TS and vice versa
"allowJs": true,
// Use correct ESM import behavior
"esModuleInterop": true,
// Disallow features that require cross-file awareness
"isolatedModules": true,
},
}
```
::: tip
It's also recommended to enable [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) which requires you to write your type imports & exports using explicit syntax. Refactoring may be necessary.
[Read more](https://www.typescriptlang.org/docs/handbook/modules/reference.html#type-only-imports-and-exports)
:::
### JSX
_tsx_ respects the following configurations for JSX in `.jsx` and `.tsx` files:
- [`jsx`](https://www.typescriptlang.org/tsconfig/#jsx)
- [`jsxFactory`](https://www.typescriptlang.org/tsconfig/#jsxFactory)
- [`jsxFragmentFactory`](https://www.typescriptlang.org/tsconfig/#jsxFragmentFactory)
- [`jsxImportSource`](https://www.typescriptlang.org/tsconfig/#jsxImportSource)
## Custom `tsconfig.json` path
By default, `tsconfig.json` is detected from the current working directory.
To pass in a `tsconfig.json` file from a custom path, use the `--tsconfig` flag:
```sh
tsx --tsconfig ./path/to/tsconfig.custom.json ./file.ts
```
## Type checking
Use TypeScript to type check:
```sh
tsc --noEmit
```
(You can omit `--noEmit` if it's already specified in your `tsconfig.json`)
### `package.json` script
Since `tsc` is also a compiler, you can add a script to your `package.json` to specify that it's used for type checking only:
```js
// package.json
{
// ...
"scripts": {
"type-check": "tsc --noEmit"// [!code ++]
},
// ...
}
```
### Pre-commit hook
To type check on pre-commit, use [simple-git-hooks](https://www.npmjs.com/package/simple-git-hooks):
```js
// package.json
{
// ...
"scripts": {
// Register Git hooks on `npm install`
"prepare": "simple-git-hooks"// [!code ++]
},
"simple-git-hooks": {
"pre-commit": "npm run type-check",// [!code ++]
// Or if you have multiple commands
"pre-commit": [
"npm run lint",
"npm run type-check"// [!code ++]
]
}
}
```
## Compiler limitations
_tsx_ uses [esbuild](https://esbuild.github.io) to compile TypeScript and ESM, so it shares some of the same limitations:
- Compatibility with `eval()` is not preserved
- Only [certain `tsconfig.json` properties](https://esbuild.github.io/content-types/#tsconfig-json) are supported
- [`emitDecoratorMetadata`](https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata) is not supported
For detailed information, refer to esbuild's [JavaScript caveats](https://esbuild.github.io/content-types/#javascript-caveats) and [TypeScript caveats](https://esbuild.github.io/content-types/#typescript-caveats) documentation.
================================================
FILE: docs/vscode.md
================================================
# VS Code debugging
If you use [VS Code](https://code.visualstudio.com), you can configure it to use _tsx_ to enhance your debugging experience.
To learn more about VS Code configuration, refer to the [_Launch Configuration_](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_launch-configuration) documentation.
## Setup
1. Create the config file
Create a launch configuration file in your project at `.vscode/launch.json`:
```json5
{
"version": "0.2.0",
"configurations": [
/*
* Each config in this array corresponds to an option
* in the debug drop-down
*/
],
}
```
2. Pick and choose debugging methods
::: details Method 1: Run _tsx_ from inside VSCode
1. Add the following configuration to the `configurations` array in `.vscode/launch.json`:
```json5
{
"name": "tsx",
"type": "node",
"request": "launch",
// Debug current file in VSCode
"program": "${file}",
/*
* Path to tsx binary
* Assuming locally installed
*/
"runtimeExecutable": "tsx",
/*
* Open terminal when debugging starts (Optional)
* Useful to see console.logs
*/
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
// Files to exclude from debugger (e.g. call stack)
"skipFiles": [
// Node.js internal core modules
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
}
```
2. In VSCode, open the JS/TS file you want to run
3. Go to VSCode's debug panel, select "tsx" in the drop down, and hit the play button (<kbd>F5</kbd>).
:::
::: details Method 2: Attach the VS Code debugger to a running Node.js process
> This method works for any Node.js process like _tsx_, and is not specific to _tsx_.
1. Add the following configuration to the `configurations` array in `.vscode/launch.json`:
```json
{
"name": "Attach to process",
"type": "node",
"request": "attach",
"port": 9229,
"skipFiles": [
// Node.js internal core modules
"<node_internals>/**",
// Ignore all dependencies (optional)
"${workspaceFolder}/node_modules/**",
],
}
```
2. Run _tsx_ with [`--inspect-brk`](https://nodejs.org/api/cli.html#--inspect-brkhostport) in a terminal window:
```sh
tsx --inspect-brk ./your-file.ts
```
3. Go to VSCode's debug panel, select "Attach to process" in the drop down, and hit the play button (<kbd>F5</kbd>).
:::
================================================
FILE: docs/watch-mode.md
================================================
# Watch mode
::: warning Not to be confused with [Node's Watch mode](https://nodejs.org/docs/latest/api/cli.html#--watch)
_tsx_ introduced _Watch mode_ before Node.js released the `--watch` flag in [v18.11.0](https://github.com/nodejs/node/releases/tag/v18.11.0). Although it is similar in functionality, it does not yet match the robustness of _tsx_'s Watch mode.
:::
## Overview
Watch mode will automatically re-run your script whenever any of its dependencies are changed.
```sh
tsx watch ./file.ts
```
## Watch behavior
By default, _tsx_ watches all imported files, except those in the following directories:
- `node_modules`
- `bower_components`
- `vendor`
- `dist`
- Hidden directories (`.*`)
## Customizing watched files
### Including files to watch
To include specific files or directories to watch, use the `--include` flag:
```sh
tsx watch --include ./other-dep.txt --include "./other-deps/*" ./file.ts
```
### Excluding files from watch
To exclude specific files from being watched, use the `--exclude` flag:
```sh
tsx watch --exclude ./ignore-me.js --exclude ./ignore-me-too.js ./file.ts
```
### Using glob patterns
Glob patterns allow you to define a set of files or directories to ignore. To prevent your shell from expanding the glob patterns, wrap them in quotes:
```sh
tsx watch --exclude "./data/**/*" ./file.ts
```
## Tips
- Press <kbd>Return</kbd> to manually rerun the script.
- Use `--clear-screen=false` to prevent the screen from clearing on rerun.
================================================
FILE: package.json
================================================
{
"name": "tsx",
"version": "0.0.0-semantic-release",
"description": "TypeScript Execute (tsx): Node.js enhanced with esbuild to run TypeScript & ESM files",
"keywords": [
"cli",
"runtime",
"node",
"cjs",
"commonjs",
"esm",
"typescript",
"typescript runner"
],
"license": "MIT",
"repository": "privatenumber/tsx",
"author": {
"name": "Hiroki Osame",
"email": "hiroki.osame@gmail.com"
},
"files": [
"dist"
],
"type": "module",
"bin": "./dist/cli.mjs",
"exports": {
"./package.json": "./package.json",
".": "./dist/loader.mjs",
"./patch-repl": "./dist/patch-repl.cjs",
"./cjs": "./dist/cjs/index.cjs",
"./cjs/api": {
"import": {
"types": "./dist/cjs/api/index.d.mts",
"default": "./dist/cjs/api/index.mjs"
},
"require": {
"types": "./dist/cjs/api/index.d.cts",
"default": "./dist/cjs/api/index.cjs"
}
},
"./esm": "./dist/esm/index.mjs",
"./esm/api": {
"import": {
"types": "./dist/esm/api/index.d.mts",
"default": "./dist/esm/api/index.mjs"
},
"require": {
"types": "./dist/esm/api/index.d.cts",
"default": "./dist/esm/api/index.cjs"
}
},
"./cli": "./dist/cli.mjs",
"./suppress-warnings": "./dist/suppress-warnings.cjs",
"./preflight": "./dist/preflight.cjs",
"./repl": "./dist/repl.mjs"
},
"packageManager": "pnpm@10.9.0",
"homepage": "https://tsx.is",
"scripts": {
"build": "pkgroll --minify",
"lint": "lintroll --node --cache --ignore-pattern 'docs/*.md' .",
"type-check": "tsc",
"test": "pnpm build && node ./dist/cli.mjs tests/index.ts",
"prepack": "pnpm build && clean-pkg-json",
"docs:dev": "pnpm --filter=docs dev",
"docs:build": "pnpm --filter=docs build",
"docs:preview": "pnpm --filter=docs preview"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"devDependencies": {
"@ampproject/remapping": "^2.3.0",
"@types/cross-spawn": "^6.0.6",
"@types/node": "^22.15.29",
"@types/split2": "^4.2.3",
"append-transform": "^2.0.0",
"cachedir": "^2.4.0",
"chokidar": "^3.6.0",
"clean-pkg-json": "^1.2.0",
"cleye": "^1.3.2",
"cross-spawn": "^7.0.3",
"es-module-lexer": "^1.5.4",
"execa": "^8.0.1",
"fs-fixture": "^2.4.0",
"fs-require": "^1.6.0",
"get-node": "^15.0.1",
"kolorist": "^1.8.0",
"lintroll": "^1.8.1",
"magic-string": "^0.30.10",
"manten": "^1.5.0",
"memfs": "^4.9.3",
"outdent": "^0.8.0",
"pkgroll": "^2.4.1",
"proxyquire": "^2.1.3",
"pty-spawn": "^1.1.0",
"strip-ansi": "^7.1.0",
"type-fest": "^4.20.1",
"type-flag": "^3.0.0",
"typescript": "^5.5.2"
},
"pnpm": {
"packageExtensions": {
"node-pty": {
"//": "https://github.com/microsoft/node-pty/issues/777",
"dependencies": {
"node-gyp": "11.0.0"
}
}
},
"onlyBuiltDependencies": [
"node-pty",
"esbuild"
]
}
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- docs
================================================
FILE: release.config.cjs
================================================
const outdent = require('outdent');
module.exports = {
addReleases: 'bottom',
successComment: outdent`
This issue is now resolved in [v\${nextRelease.version}](https://github.com/privatenumber/tsx/releases/tag/v\${nextRelease.version}).
If you're able to, [your sponsorship](https://github.com/sponsors/privatenumber) would be very much appreciated.
`,
};
================================================
FILE: src/@types/es-module-lexer.d.ts
================================================
declare module 'es-module-lexer/js' {
export { parse } from 'es-module-lexer';
}
================================================
FILE: src/@types/module.d.ts
================================================
import 'node:module';
declare global {
namespace NodeJS {
export interface Module {
_compile(code: string, filename: string): string;
}
}
}
declare module 'module' {
// CommonJS
export const _extensions: NodeJS.RequireExtensions;
export const _cache: NodeJS.Require['cache'];
export type Parent = {
id: string;
/**
* Can be null if the parent id is 'internal/preload' (e.g. via --require)
* which doesn't have a file path.
*/
filename: string | null;
path: string;
paths: string[];
};
export function _resolveFilename(
request: string,
parent: Parent | undefined,
isMain?: boolean,
options?: Record<PropertyKey, unknown>,
): string;
export function _nodeModulePaths(path: string): string[];
interface LoadFnOutput {
// Added in https://github.com/nodejs/node/pull/43164
responseURL?: string;
}
}
================================================
FILE: src/cjs/api/index.ts
================================================
export { register } from './register.js';
export { require } from './require.js';
================================================
FILE: src/cjs/api/module-extensions.ts
================================================
import fs from 'node:fs';
import path from 'node:path';
import Module from 'node:module';
import type { TransformOptions } from 'esbuild';
import { transformSync } from '../../utils/transform/index.js';
import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js';
import { isESM } from '../../utils/es-module-lexer.js';
import { shouldApplySourceMap, inlineSourceMap } from '../../source-map.js';
import { parent } from '../../utils/ipc/client.js';
import { fileMatcher } from '../../utils/tsconfig.js';
import { logCjs as log } from '../../utils/debug.js';
import type { LoaderState } from './types.js';
const typescriptExtensions = [
'.cts',
'.mts',
'.ts',
'.tsx',
'.jsx',
] as const;
const transformExtensions = [
'.js',
'.cjs',
'.mjs',
] as const;
const implicitlyResolvableExtensions = [
'.ts',
'.tsx',
'.jsx',
] as const;
const safeSet = <T extends Record<string, unknown>>(
object: T,
property: keyof T,
value: T[keyof T],
descriptor?: {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
},
) => {
const existingDescriptor = Object.getOwnPropertyDescriptor(object, property);
// If setter is provided, use it
if (existingDescriptor?.set) {
object[property] = value;
} else if (
!existingDescriptor
|| existingDescriptor.configurable
) {
Object.defineProperty(object, property, {
value,
enumerable: existingDescriptor?.enumerable || descriptor?.enumerable,
writable: (
descriptor?.writable
?? (
existingDescriptor
? existingDescriptor.writable
: true
)
),
configurable: (
descriptor?.configurable
?? (
existingDescriptor
? existingDescriptor.configurable
: true
)
),
});
}
};
export const createExtensions = (
state: LoaderState,
extensions: NodeJS.RequireExtensions,
namespace?: string,
) => {
const defaultLoader = extensions['.js'];
const transformer = (
module: Module,
filePath: string,
) => {
if (state.enabled === false) {
return defaultLoader(module, filePath);
}
// Make sure __filename doesnt contain query
const [cleanFilePath, query] = filePath.split('?');
const searchParams = new URLSearchParams(query);
// If request namespace doesnt match the namespace, ignore
if ((searchParams.get('namespace') ?? undefined) !== namespace) {
return defaultLoader(module, filePath);
}
log(2, 'load', {
filePath,
});
/**
* In new Module(), m.path = path.dirname(module.id) but module.id coming from
* ESM resolver may be a data: path
*
* In these cases, we fix m.path to be the actual directory of the file
*/
// https://github.com/nodejs/node/blob/v22.8.0/lib/internal/modules/cjs/loader.js#L298
if (module.id.startsWith('data:text/javascript,')) {
module.path = path.dirname(cleanFilePath);
}
// For tracking dependencies in watch mode
if (parent?.send) {
parent.send({
type: 'dependency',
path: cleanFilePath,
});
}
const transformTs = typescriptExtensions.some(extension => cleanFilePath.endsWith(extension));
const transformJs = transformExtensions.some(extension => cleanFilePath.endsWith(extension));
if (!transformTs && !transformJs) {
return defaultLoader(module, cleanFilePath);
}
let code = fs.readFileSync(cleanFilePath, 'utf8');
if (cleanFilePath.endsWith('.cjs')) {
// Contains native ESM check
const transformed = transformDynamicImport(filePath, code);
if (transformed) {
code = (
shouldApplySourceMap()
? inlineSourceMap(transformed)
: transformed.code
);
}
} else if (
transformTs
// CommonJS file but uses ESM import/export
|| isESM(code)
) {
const transformed = transformSync(
code,
filePath,
{
tsconfigRaw: fileMatcher?.(cleanFilePath) as TransformOptions['tsconfigRaw'],
},
);
code = (
shouldApplySourceMap()
? inlineSourceMap(transformed)
: transformed.code
);
}
log(1, 'loaded', {
filePath: cleanFilePath,
});
module._compile(code, cleanFilePath);
};
/**
* Handles .cjs, .cts, .mts & any explicitly specified extension that doesn't match any loaders
*
* Any file requested with an explicit extension will be loaded using the .js loader:
* https://github.com/nodejs/node/blob/e339e9c5d71b72fd09e6abd38b10678e0c592ae7/lib/internal/modules/cjs/loader.js#L430
*/
safeSet(extensions, '.js', transformer);
for (const extension of implicitlyResolvableExtensions) {
safeSet(extensions, extension, transformer, {
/**
* Registration needs to be enumerable for some 3rd party libraries
* https://github.com/gulpjs/rechoir/blob/v0.8.0/index.js#L21 (used by Webpack CLI)
*
* If the extension already exists, inherit its enumerable property
* If not, only expose if it's not namespaced
*/
enumerable: !namespace,
writable: true,
configurable: true,
});
}
/**
* Loaders for extensions .cjs, .cts, & .mts don't need to be
* registered because they're explicitly specified. And unknown
* extensions (incl .cjs) fallsback to using the '.js' loader:
* https://github.com/nodejs/node/blob/v18.4.0/lib/internal/modules/cjs/loader.js#L430
*
* That said, it's actually ".js" and ".mjs" that get special treatment
* rather than ".cjs" (it might as well be ".random-ext")
*/
safeSet(extensions, '.mjs', transformer, {
/**
* enumerable defaults to whatever is already set, but if not set, it's false
*
* This prevent Object.keys from detecting these extensions
* when CJS loader iterates over the possible extensions
* https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/cjs/loader.js#L609
*/
writable: true,
configurable: true,
});
// Unregister
return () => {
/**
* The extensions are only reverted if they're still tsx's transformers
*
* Otherwise, it means they have been wrapped by another loader and should
* be left untouched not to remove the other loader
*/
if (extensions['.js'] === transformer) {
extensions['.js'] = defaultLoader;
}
for (const extension of [...implicitlyResolvableExtensions, '.mjs']) {
if (extensions[extension] === transformer) {
delete extensions[extension];
}
}
};
};
================================================
FILE: src/cjs/api/module-resolve-filename/index.ts
================================================
import Module from 'node:module';
import { fileURLToPath } from 'node:url';
import {
isFilePath,
fileUrlPrefix,
tsExtensionsPattern,
nodeModulesPath,
} from '../../../utils/path-utils.js';
import { tsconfigPathsMatcher } from '../../../utils/tsconfig.js';
import type { ResolveFilename, SimpleResolve, LoaderState } from '../types.js';
import { logCjs as log } from '../../../utils/debug.js';
import { createImplicitResolver } from './resolve-implicit-extensions.js';
import { interopCjsExports } from './interop-cjs-exports.js';
import { createTsExtensionResolver } from './resolve-ts-extensions.js';
import { preserveQuery } from './preserve-query.js';
const resolveTsPaths = (
request: string,
parent: Module.Parent | undefined,
nextResolve: SimpleResolve,
) => {
// Support file protocol
if (request.startsWith(fileUrlPrefix)) {
request = fileURLToPath(request);
}
// Resolve TS path alias
if (
tsconfigPathsMatcher
// bare specifier
&& !isFilePath(request)
// Dependency paths should not be resolved using tsconfig.json
&& !parent?.filename?.includes(nodeModulesPath)
) {
const possiblePaths = tsconfigPathsMatcher(request);
for (const possiblePath of possiblePaths) {
try {
return nextResolve(possiblePath);
} catch {}
}
}
return nextResolve(request);
};
export const createResolveFilename = (
state: LoaderState,
nextResolve: ResolveFilename,
namespace?: string,
): ResolveFilename => (
request,
parent,
...restOfArgs
) => {
if (state.enabled === false) {
return nextResolve(request, parent, ...restOfArgs);
}
request = interopCjsExports(request);
const [
cleanRequest,
searchParams,
appendQuery,
] = preserveQuery(request, parent);
// If request namespace doesnt match the namespace, ignore
if ((searchParams.get('namespace') ?? undefined) !== namespace) {
return nextResolve(request, parent, ...restOfArgs);
}
log(2, 'resolve', {
request,
parent: parent?.filename ?? parent,
restOfArgs,
});
let nextResolveSimple: SimpleResolve = request_ => nextResolve(
request_,
parent,
...restOfArgs,
);
nextResolveSimple = createTsExtensionResolver(
nextResolveSimple,
Boolean(
// If register.namespace is used (e.g. tsx.require())
namespace
// If parent is a TS file
|| (parent?.filename && tsExtensionsPattern.test(parent.filename)),
),
);
nextResolveSimple = createImplicitResolver(nextResolveSimple);
const resolved = appendQuery(
resolveTsPaths(cleanRequest, parent, nextResolveSimple),
restOfArgs.length,
);
log(1, 'resolved', {
request,
parent: parent?.filename ?? parent,
resolved,
});
return resolved;
};
================================================
FILE: src/cjs/api/module-resolve-filename/interop-cjs-exports.ts
================================================
import Module from 'node:module';
export const getOriginalFilePath = (
request: string,
) => {
if (!request.startsWith('data:text/javascript,')) {
return;
}
const queryIndex = request.indexOf('?');
if (queryIndex === -1) {
return;
}
const searchParams = new URLSearchParams(request.slice(queryIndex + 1));
const filePath = searchParams.get('filePath');
if (filePath) {
return filePath;
}
};
export const interopCjsExports = (
request: string,
) => {
const filePath = getOriginalFilePath(request);
if (filePath) {
// The CJS module cache needs to be updated with the actual path for export parsing to work
// https://github.com/nodejs/node/blob/v22.2.0/lib/internal/modules/esm/translators.js#L338
Module._cache[filePath] = Module._cache[request];
delete Module._cache[request];
request = filePath;
}
return request;
};
================================================
FILE: src/cjs/api/module-resolve-filename/is-from-cjs-lexer.ts
================================================
const cjsPreparseCall = 'at cjsPreparseModuleExports (node:internal';
export const isFromCjsLexer = (
error: Error,
) => {
const stack = error.stack!.split('\n').slice(1);
return (
stack[1].includes(cjsPreparseCall)
|| stack[2].includes(cjsPreparseCall)
);
};
================================================
FILE: src/cjs/api/module-resolve-filename/preserve-query.ts
================================================
import path from 'node:path';
import Module from 'node:module';
import { urlSearchParamsStringify } from '../../../utils/url-search-params-stringify.js';
import { isFromCjsLexer } from './is-from-cjs-lexer.js';
import { getOriginalFilePath } from './interop-cjs-exports.js';
export const preserveQuery = (
request: string,
parent?: Module.Parent,
) => {
// Strip query string
const requestAndQuery = request.split('?');
const searchParams = new URLSearchParams(requestAndQuery[1]);
if (parent?.filename) {
const filePath = getOriginalFilePath(parent.filename);
let query: string | undefined;
if (filePath) {
const pathAndQuery = filePath.split('?');
const newFilename = pathAndQuery[0];
query = pathAndQuery[1];
/**
* Can't delete the old cache entry because there's an assertion
* https://github.com/nodejs/node/blob/v20.15.0/lib/internal/modules/esm/translators.js#L347
*/
// delete Module._cache[parent.filename];
parent.filename = newFilename;
parent.path = path.dirname(newFilename);
// https://github.com/nodejs/node/blob/v20.15.0/lib/internal/modules/esm/translators.js#L383
parent.paths = Module._nodeModulePaths(parent.path);
Module._cache[newFilename] = parent as NodeModule;
}
if (!query) {
query = parent.filename.split('?')[1];
}
// Inherit parent namespace if it exists
const parentQuery = new URLSearchParams(query);
const parentNamespace = parentQuery.get('namespace');
if (parentNamespace) {
searchParams.append('namespace', parentNamespace);
}
}
return [
requestAndQuery[0],
searchParams,
(
resolved: string,
restOfArgsLength: number,
) => {
// Only add query back if it's a file path (not a core Node module)
if (
path.isAbsolute(resolved)
// These two have native loaders which don't support queries
&& !resolved.endsWith('.json')
&& !resolved.endsWith('.node')
/**
* Detect if this is called by the CJS lexer, the resolved path is directly passed into
* readFile to parse the exports
*/
&& !(
// Only the CJS lexer doesn't pass in the rest of the arguments
// https://github.com/nodejs/node/blob/v20.15.0/lib/internal/modules/esm/translators.js#L415
restOfArgsLength === 0
// eslint-disable-next-line unicorn/error-message
&& isFromCjsLexer(new Error())
)
) {
resolved += urlSearchParamsStringify(searchParams);
}
return resolved;
},
] as const;
};
================================================
FILE: src/cjs/api/module-resolve-filename/resolve-implicit-extensions.ts
================================================
import path from 'node:path';
import type { NodeError } from '../../../types.js';
import { isDirectoryPattern } from '../../../utils/path-utils.js';
import type { SimpleResolve } from '../types.js';
/**
* Custom re-implementation of the CommonJS implicit resolver
*
* - Resolves .ts over .js extensions
* - When namespaced, the loaders are registered to the extensions in a hidden way
* so Node's built-in implicit resolver will not try those extensions
*/
export const createImplicitResolver = (
nextResolve: SimpleResolve,
): SimpleResolve => (
request,
) => {
if (request === '.' || request === '..' || request.endsWith('/..')) {
request += '/';
}
/**
* Currently, there's an edge case where it doesn't resolve index.ts over index.js
* if the request doesn't end with a slash. e.g. `import './dir'`
* Doesn't handle '.' either
*/
if (isDirectoryPattern.test(request)) {
// If directory, can be index.js, index.ts, etc.
let joinedPath = path.join(request, 'index.js');
/**
* path.join will remove the './' prefix if it exists
* but it should only be added back if it was there before
* (e.g. not package directory imports)
*/
if (request.startsWith('./')) {
joinedPath = `./${joinedPath}`;
}
try {
return nextResolve(joinedPath);
} catch {}
}
try {
return nextResolve(request);
} catch (_error) {
const nodeError = _error as NodeError;
if (nodeError.code === 'MODULE_NOT_FOUND') {
try {
return nextResolve(`${request}${path.sep}index.js`);
} catch {}
}
throw nodeError;
}
};
================================================
FILE: src/cjs/api/module-resolve-filename/resolve-ts-extensions.ts
================================================
import { mapTsExtensions } from '../../../utils/map-ts-extensions.js';
import type { NodeError } from '../../../types.js';
import {
isFilePath,
isDirectoryPattern,
} from '../../../utils/path-utils.js';
import { allowJs } from '../../../utils/tsconfig.js';
import type { SimpleResolve } from '../types.js';
import { logCjs as log } from '../../../utils/debug.js';
/**
* Typescript gives .ts, .cts, or .mts priority over actual .js, .cjs, or .mjs extensions
*/
const resolveTsFilename = (
resolve: SimpleResolve,
request: string,
isTsParent: boolean,
) => {
log(3, 'resolveTsFilename', {
request,
isDirectory: isDirectoryPattern.test(request),
isTsParent,
allowJs,
});
if (
isDirectoryPattern.test(request)
|| (!isTsParent && !allowJs)
) {
return;
}
const tsPath = mapTsExtensions(request);
if (!tsPath) {
return;
}
for (const tryTsPath of tsPath) {
try {
return resolve(tryTsPath);
} catch (error) {
const { code } = error as NodeError;
if (
code !== 'MODULE_NOT_FOUND'
&& code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
) {
throw error;
}
}
}
};
export const createTsExtensionResolver = (
nextResolve: SimpleResolve,
isTsParent: boolean,
): SimpleResolve => (
request,
) => {
log(3, 'resolveTsFilename', {
request,
isTsParent,
isFilePath: isFilePath(request),
});
// It should only try to resolve TS extensions first if it's a local file (non dependency)
if (isFilePath(request)) {
const resolvedTsFilename = resolveTsFilename(nextResolve, request, isTsParent);
if (resolvedTsFilename) {
return resolvedTsFilename;
}
}
try {
return nextResolve(request);
} catch (error) {
const nodeError = error as NodeError;
if (nodeError.code === 'MODULE_NOT_FOUND') {
// Exports map resolution
if (nodeError.path) {
const isExportsPath = nodeError.message.match(/^Cannot find module '([^']+)'$/);
if (isExportsPath) {
const exportsPath = isExportsPath[1];
const tsFilename = resolveTsFilename(nextResolve, exportsPath, isTsParent);
if (tsFilename) {
return tsFilename;
}
}
const isMainPath = nodeError.message.match(/^Cannot find module '([^']+)'. Please verify that the package.json has a valid "main" entry$/);
if (isMainPath) {
const mainPath = isMainPath[1];
const tsFilename = resolveTsFilename(nextResolve, mainPath, isTsParent);
if (tsFilename) {
return tsFilename;
}
}
}
const resolvedTsFilename = resolveTsFilename(nextResolve, request, isTsParent);
if (resolvedTsFilename) {
return resolvedTsFilename;
}
}
throw nodeError;
}
};
================================================
FILE: src/cjs/api/register.ts
================================================
import Module from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadTsconfig } from '../../utils/tsconfig.js';
import type { RequiredProperty } from '../../types.js';
import { urlSearchParamsStringify } from '../../utils/url-search-params-stringify.js';
import { fileUrlPrefix } from '../../utils/path-utils.js';
import type { LoaderState } from './types.js';
import { createExtensions } from './module-extensions.js';
import { createResolveFilename } from './module-resolve-filename/index.js';
const resolveContext = (
id: string,
fromFile: string | URL,
) => {
if (!fromFile) {
throw new Error('The current file path (__filename or import.meta.url) must be provided in the second argument of tsx.require()');
}
// If id is not a relative path, it doesn't need to be resolved
if (!id.startsWith('.')) {
return id;
}
if (
(typeof fromFile === 'string' && fromFile.startsWith(fileUrlPrefix))
|| fromFile instanceof URL
) {
fromFile = fileURLToPath(fromFile);
}
return path.resolve(path.dirname(fromFile), id);
};
type RegisterOptions = {
namespace?: string;
};
export type Unregister = () => void;
type ScopedRequire = (
id: string,
fromFile: string | URL,
) => any; // eslint-disable-line @typescript-eslint/no-explicit-any
type ScopedResolve = (
id: string,
fromFile: string | URL,
resolveOptions?: { paths?: string[] | undefined },
) => string;
export type NamespacedUnregister = Unregister & {
require: ScopedRequire;
resolve: ScopedResolve;
unregister: Unregister;
};
export type Register = {
(options: RequiredProperty<RegisterOptions, 'namespace'>): NamespacedUnregister;
(options?: RegisterOptions): Unregister;
};
export const register: Register = (
options,
) => {
const { sourceMapsEnabled } = process;
const state: LoaderState = {
enabled: true,
};
loadTsconfig(process.env.TSX_TSCONFIG_PATH);
// register
process.setSourceMapsEnabled(true);
const originalResolveFilename = Module._resolveFilename;
const resolveFilename = createResolveFilename(state, originalResolveFilename, options?.namespace);
Module._resolveFilename = resolveFilename;
const unregisterExtensions = createExtensions(state, Module._extensions, options?.namespace);
const unregister = () => {
if (sourceMapsEnabled === false) {
process.setSourceMapsEnabled(false);
}
state.enabled = false;
/**
* Only revert the _resolveFilename & extensions if they're unwrapped
* by another loader extension
*/
if (Module._resolveFilename === resolveFilename) {
Module._resolveFilename = originalResolveFilename;
}
unregisterExtensions();
};
if (options?.namespace) {
const scopedRequire: ScopedRequire = (id, fromFile) => {
const resolvedId = resolveContext(id, fromFile);
const [request, query] = resolvedId.split('?');
const parameters = new URLSearchParams(query);
if (options.namespace && !request.startsWith('node:')) {
parameters.set('namespace', options.namespace);
}
// eslint-disable-next-line @typescript-eslint/no-require-imports, import-x/no-dynamic-require
return require(request + urlSearchParamsStringify(parameters));
};
unregister.require = scopedRequire;
const scopedResolve: ScopedResolve = (id, fromFile, resolveOptions) => {
const resolvedId = resolveContext(id, fromFile);
const [request, query] = resolvedId.split('?');
const parameters = new URLSearchParams(query);
if (options.namespace && !request.startsWith('node:')) {
parameters.set('namespace', options.namespace);
}
return resolveFilename(
request + urlSearchParamsStringify(parameters),
module,
false,
resolveOptions,
);
};
unregister.resolve = scopedResolve;
unregister.unregister = unregister;
}
return unregister;
};
================================================
FILE: src/cjs/api/require.ts
================================================
import { register, type NamespacedUnregister } from './register.js';
let api: NamespacedUnregister | undefined;
const tsxRequire = (
id: string,
fromFile: string | URL,
) => {
if (!api) {
api = register({
namespace: Date.now().toString(),
});
}
return api.require(id, fromFile);
};
const resolve = (
id: string,
fromFile: string | URL,
options?: { paths?: string[] | undefined },
) => {
if (!api) {
api = register({
namespace: Date.now().toString(),
});
}
return api.resolve(id, fromFile, options);
};
resolve.paths = require.resolve.paths;
tsxRequire.resolve = resolve;
tsxRequire.main = require.main;
tsxRequire.extensions = require.extensions;
tsxRequire.cache = require.cache;
export { tsxRequire as require };
================================================
FILE: src/cjs/api/types.ts
================================================
import type Module from 'node:module';
export type LoaderState = {
enabled: boolean;
};
export type ResolveFilename = typeof Module._resolveFilename;
export type SimpleResolve = (request: string) => string;
================================================
FILE: src/cjs/index.ts
================================================
import { register } from './api/register.js';
register();
================================================
FILE: src/cli.ts
================================================
import { constants as osConstants } from 'node:os';
import type { ChildProcess, Serializable } from 'node:child_process';
import type { Server } from 'node:net';
import { cli } from 'cleye';
import {
transformSync as esbuildTransformSync,
} from 'esbuild';
import { version } from '../package.json';
import { run } from './run.js';
import { watchCommand } from './watch/index.js';
import {
removeArgvFlags,
ignoreAfterArgument,
} from './remove-argv-flags.js';
import { isFeatureSupported, testRunnerGlob } from './utils/node-features.js';
import { createIpcServer } from './utils/ipc/server.js';
// const debug = (...messages: any[]) => {
// if (process.env.DEBUG) {
// console.log(...messages);
// }
// };
const relaySignals = (
childProcess: ChildProcess,
ipcSocket: Server,
) => {
let waitForSignal: undefined | ((signal: NodeJS.Signals) => void);
ipcSocket.on(
'data',
(
data: {
type: string;
signal: NodeJS.Signals;
},
) => {
if (
data
&& data.type === 'signal'
&& waitForSignal
) {
waitForSignal(data.signal);
}
},
);
/**
* Wait for signal from preflight bindHiddenSignalsHandler
* Ideally the timeout should be as low as possible
* since the child lets the parent know that it received
* the signal
*/
const waitForSignalFromChild = () => {
const p = new Promise<NodeJS.Signals | undefined>((resolve) => {
// Aribrary timeout based on flaky tests
setTimeout(() => resolve(undefined), 30);
waitForSignal = resolve;
});
p.then(
() => {
waitForSignal = undefined;
},
() => {},
);
return p;
};
const relaySignalToChild = async (
signal: NodeJS.Signals,
) => {
/**
* This callback is triggered if the parent receives a signal
*
* Child could also receive a signal at the same time if it detected
* a keypress or was sent a signal via process group
*
* The preflight registers a signal handler on the child to
* tell the parent if it also received a signal which we wait for here
*/
const signalFromChild = await waitForSignalFromChild();
// debug({
// signalFromChild,
// });
/**
* If child didn't receive a signal, it's either because it was
* sent to the parent directly via kill PID or the child is
* unresponsive (e.g. infinite loop). Relay signal to child.
*/
if (signalFromChild !== signal) {
// debug('killing child', {
// signal,
// });
childProcess.kill(signal);
/**
* If child is unresponsive (e.g. infinite loop), we need to force kill it
*/
const isChildResponsive = await waitForSignalFromChild();
if (isChildResponsive !== signal) {
// This seems to run before the handler registered at the bottom of this file
// Seems the lastest handler is called first
childProcess.on('exit', () => {
/**
* Even though this may not be a SIGKILL, I've confirmed Ctrl+C on an infinite looping
* file exits with 130, which is 128 + 2 (SIGINT)
*
* https://nodejs.org/api/process.html#exit-codes
* >128 Signal Exits: If Node.js receives a fatal signal such as SIGKILL or SIGHUP,
* then its exit code will be 128 plus the value of the signal code. This is a
* standard POSIX practice, since exit codes are defined to be 7-bit integers, and
* signal exits set the high-order bit, and then contain the value of the signal code.
* For example, signal SIGABRT has value 6, so the expected exit code will be 128 + 6,
* or 134.
*/
const exitCode = osConstants.signals[signal];
process.exit(128 + exitCode);
});
childProcess.kill('SIGKILL');
}
}
};
process.on('SIGINT', relaySignalToChild);
process.on('SIGTERM', relaySignalToChild);
};
const tsxFlags = {
noCache: {
type: Boolean,
description: 'Disable caching',
},
tsconfig: {
type: String,
description: 'Custom tsconfig.json path',
},
};
cli({
name: 'tsx',
parameters: ['[script path]'],
commands: [
watchCommand,
],
flags: {
...tsxFlags,
version: {
type: Boolean,
alias: 'v',
description: 'Show version',
},
help: {
type: Boolean,
alias: 'h',
description: 'Show help',
},
},
help: false,
ignoreArgv: ignoreAfterArgument(),
}, async (argv) => {
if (argv.flags.version) {
process.stdout.write(`tsx v${version}\nnode `);
} else if (argv.flags.help) {
argv.showHelp({
description: 'Node.js runtime enhanced with esbuild for loading TypeScript & ESM',
});
console.log(`${'-'.repeat(45)}\n`);
}
const interceptFlags = {
eval: {
type: String,
alias: 'e',
},
print: {
type: String,
alias: 'p',
},
} as const;
const {
_: firstArgs,
flags: interceptedFlags,
} = cli({
flags: {
...interceptFlags,
inputType: String,
test: Boolean,
},
help: false,
ignoreArgv: ignoreAfterArgument(false),
});
const argvsToRun = removeArgvFlags({
...tsxFlags,
...interceptFlags,
});
const evalTypes = ['print', 'eval'] as const;
const evalType = evalTypes.find(type => Boolean(interceptedFlags[type]));
if (evalType) {
const { inputType } = interceptedFlags;
const evalCode = interceptedFlags[evalType]!;
const transformed = esbuildTransformSync(
evalCode,
{
loader: 'default',
sourcefile: '/eval.ts',
format: inputType === 'module' ? 'esm' : 'cjs',
},
);
argvsToRun.unshift(`--${evalType}`, transformed.code);
}
// Default --test glob to find TypeScript files
if (
isFeatureSupported(testRunnerGlob)
&& interceptedFlags.test
&& firstArgs.length === 0
) {
argvsToRun.push('**/{test,test/**/*,test-*,*[.-_]test}.?(c|m)@(t|j)s');
}
const ipc = await createIpcServer();
const childProcess = run(
argvsToRun,
{
noCache: Boolean(argv.flags.noCache),
tsconfigPath: argv.flags.tsconfig,
},
);
relaySignals(childProcess, ipc);
if (process.send) {
childProcess.on('message', (message) => {
process.send!(message);
});
}
if (childProcess.send) {
process.on('message', (message) => {
childProcess.send(message as Serializable);
});
}
childProcess.on(
'close',
(exitCode) => {
// If there's no exit code, it's likely killed by a signal
// https://nodejs.org/api/process.html#process_exit_codes
if (exitCode === null) {
exitCode = osConstants.signals[childProcess.signalCode!] + 128;
}
process.exit(exitCode);
},
);
});
================================================
FILE: src/esm/api/index.ts
================================================
export {
register,
type InitializationOptions,
type NamespacedUnregister,
type Register,
type RegisterOptions,
type Unregister,
} from './register.js';
export type { ScopedImport } from './scoped-import.js';
export { tsImport } from './ts-import.js';
================================================
FILE: src/esm/api/register.ts
================================================
import module from 'node:module';
import { MessageChannel, type MessagePort } from 'node:worker_threads';
import type { Message } from '../types.js';
import type { RequiredProperty } from '../../types.js';
import { interopCjsExports } from '../../cjs/api/module-resolve-filename/interop-cjs-exports.js';
import { createScopedImport, type ScopedImport } from './scoped-import.js';
export type TsconfigOptions = false | string;
export type InitializationOptions = {
namespace?: string;
port?: MessagePort;
tsconfig?: TsconfigOptions;
};
export type RegisterOptions = {
namespace?: string;
onImport?: (url: string) => void;
tsconfig?: TsconfigOptions;
};
export type Unregister = () => Promise<void>;
export type NamespacedUnregister = Unregister & {
import: ScopedImport;
unregister: Unregister;
};
export type Register = {
(options: RequiredProperty<RegisterOptions, 'namespace'>): NamespacedUnregister;
(options?: RegisterOptions): Unregister;
};
let cjsInteropApplied = false;
export const register: Register = (
options,
) => {
if (!module.register) {
throw new Error(`This version of Node.js (${process.version}) does not support module.register(). Please upgrade to Node v18.19 or v20.6 and above.`);
}
if (!cjsInteropApplied) {
const { _resolveFilename } = module;
module._resolveFilename = (
request,
...restOfArgs
) => _resolveFilename(
interopCjsExports(request),
...restOfArgs,
);
cjsInteropApplied = true;
}
const { sourceMapsEnabled } = process;
process.setSourceMapsEnabled(true);
const { port1, port2 } = new MessageChannel();
module.register(
// Load new copy of loader so it can be registered multiple times
`./esm/index.mjs?${Date.now()}`,
{
parentURL: import.meta.url,
data: {
port: port2,
namespace: options?.namespace,
tsconfig: options?.tsconfig,
} satisfies InitializationOptions,
transferList: [port2],
},
);
const onImport = options?.onImport;
const importHandler = onImport && ((message: Message) => {
if (message.type === 'load') {
onImport(message.url);
}
});
if (importHandler) {
port1.on('message', importHandler);
port1.unref();
}
// unregister
const unregister = () => {
if (sourceMapsEnabled === false) {
process.setSourceMapsEnabled(false);
}
if (importHandler) {
port1.off('message', importHandler);
}
port1.postMessage('deactivate');
// Not necessary to wait, but provide the option
return new Promise<void>((resolve) => {
const onDeactivated = (message: Message) => {
if (message.type === 'deactivated') {
resolve();
port1.off('message', onDeactivated);
}
};
port1.on('message', onDeactivated);
});
};
if (options?.namespace) {
unregister.import = createScopedImport(options.namespace);
unregister.unregister = unregister;
}
return unregister;
};
================================================
FILE: src/esm/api/scoped-import.ts
================================================
import { pathToFileURL } from 'node:url';
import type { TsxRequest } from '../types.js';
import { fileUrlPrefix } from '../../utils/path-utils.js';
export type ScopedImport = (
specifier: string,
parent: string,
) => Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
export const createScopedImport = (
namespace: string,
): ScopedImport => (
specifier,
parent,
) => {
if (!parent) {
throw new Error('The current file path (import.meta.url) must be provided in the second argument of tsImport()');
}
const parentURL = (
parent.startsWith(fileUrlPrefix)
? parent
: pathToFileURL(parent).toString()
);
return import(
`tsx://${JSON.stringify({
specifier,
parentURL,
namespace,
} satisfies TsxRequest)}`
);
};
================================================
FILE: src/esm/api/ts-import.ts
================================================
import { register as cjsRegister } from '../../cjs/api/index.js';
import { isFeatureSupported, esmLoadReadFile } from '../../utils/node-features.js';
import { isBarePackageNamePattern, cjsExtensionPattern } from '../../utils/path-utils.js';
import { register, type TsconfigOptions } from './register.js';
type Options = {
parentURL: string;
onImport?: (url: string) => void;
tsconfig?: TsconfigOptions;
};
const tsImport = (
specifier: string,
options: string | Options,
) => {
if (
!options
|| (typeof options === 'object' && !options.parentURL)
) {
throw new Error('The current file path (import.meta.url) must be provided in the second argument of tsImport()');
}
const isOptionsString = typeof options === 'string';
const parentURL = isOptionsString ? options : options.parentURL;
const namespace = Date.now().toString();
// Keep registered for hanging require() calls
const cjs = cjsRegister({
namespace,
});
/**
* In Node v18, the loader doesn't support reading the CommonJS from
* a data URL, so it can't actually relay the namespace. This is a workaround
* to preemptively determine whether the file is a CommonJS file, and shortcut
* to using the CommonJS loader instead of going through the ESM loader first
*/
if (
!isFeatureSupported(esmLoadReadFile)
&& (
!isBarePackageNamePattern.test(specifier)
&& cjsExtensionPattern.test(specifier)
)
) {
return Promise.resolve(cjs.require(specifier, parentURL));
}
/**
* We don't want to unregister this after load since there can be child import() calls
* that need TS support
*
* This is not accessible to others because of the namespace
*/
const api = register({
namespace,
...(
isOptionsString
? {}
: options
),
});
return api.import(specifier, parentURL);
};
/**
* Considered implmenting import.meta.resolve(), but natively, it doesn't seem to actully
* resolve relative file paths.
*
* For example, this doesn't throw: import.meta.resolve('./missing-file')
*/
// tsImport.meta = {
// resolve: (
// specifier: string,
// fromFile: string,
// ) => {
// const resolvedUrl = resolveSpecifier(specifier, fromFile);
// const unregister = register();
// try {
// return import.meta.resolve(resolvedUrl);
// } finally {
// unregister();
// }
// }
// };
export { tsImport };
================================================
FILE: src/esm/hook/index.ts
================================================
export { initialize, globalPreload } from './initialize.js';
export { load } from './load.js';
export { resolve } from './resolve.js';
================================================
FILE: src/esm/hook/initialize.ts
================================================
import type { InitializeHook } from 'node:module';
import type { InitializationOptions } from '../api/register.js';
import type { Message } from '../types.js';
import { loadTsconfig } from '../../utils/tsconfig.js';
type Data = InitializationOptions & {
active: boolean;
};
export const data: Data = {
active: true,
};
export const initialize: InitializeHook = async (
options?: InitializationOptions,
) => {
if (!options) {
throw new Error('tsx must be loaded with --import instead of --loader\nThe --loader flag was deprecated in Node v20.6.0 and v18.19.0');
}
data.namespace = options.namespace;
if (options.tsconfig !== false) {
loadTsconfig(options.tsconfig ?? process.env.TSX_TSCONFIG_PATH);
}
if (options.port) {
data.port = options.port;
// Unregister
options.port.on('message', (message: string) => {
if (message === 'deactivate') {
data.active = false;
options.port!.postMessage({ type: 'deactivated' } satisfies Message);
}
});
}
};
type GlobalPreloadHook = () => string;
// Replaced by `initialize` in Node v20.6.0, v18.19.0
export const globalPreload: GlobalPreloadHook = () => {
loadTsconfig(process.env.TSX_TSCONFIG_PATH);
return 'process.setSourceMapsEnabled(true);';
};
================================================
FILE: src/esm/hook/load.ts
================================================
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import type { LoadHook } from 'node:module';
import { readFile } from 'node:fs/promises';
import type { TransformOptions } from 'esbuild';
import { transform, transformSync } from '../../utils/transform/index.js';
import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js';
import { inlineSourceMap } from '../../source-map.js';
import { isFeatureSupported, importAttributes, esmLoadReadFile } from '../../utils/node-features.js';
import { parent } from '../../utils/ipc/client.js';
import type { Message } from '../types.js';
import { fileMatcher } from '../../utils/tsconfig.js';
import { isJsonPattern, tsExtensionsPattern, fileUrlPrefix } from '../../utils/path-utils.js';
import { isESM } from '../../utils/es-module-lexer.js';
import { logEsm as log, debugEnabled } from '../../utils/debug.js';
import { getNamespace } from './utils.js';
import { data } from './initialize.js';
const importAttributesProperty = (
isFeatureSupported(importAttributes)
? 'importAttributes'
: 'importAssertions' as 'importAttributes'
);
// eslint-disable-next-line import-x/no-mutable-exports
let load: LoadHook = async (
url,
context,
nextLoad,
) => {
if (!data.active) {
return nextLoad(url, context);
}
const urlNamespace = getNamespace(url);
if (data.namespace && data.namespace !== urlNamespace) {
return nextLoad(url, context);
}
if (data.port) {
const parsedUrl = new URL(url);
parsedUrl.searchParams.delete('tsx-namespace');
data.port.postMessage({
type: 'load',
url: parsedUrl.toString(),
} satisfies Message);
}
/*
Filter out node:*
Maybe only handle files that start with file://
*/
if (parent.send) {
parent.send({
type: 'dependency',
path: url,
});
}
if (isJsonPattern.test(url)) {
let contextAttributes = context[importAttributesProperty];
if (!contextAttributes) {
contextAttributes = {};
context[importAttributesProperty] = contextAttributes;
}
if (!contextAttributes.type) {
contextAttributes.type = 'json';
}
}
const loaded = await nextLoad(url, context);
log(3, 'loaded by next loader', {
url,
loaded,
});
const filePath = url.startsWith(fileUrlPrefix) ? fileURLToPath(url) : url;
if (
loaded.format === 'commonjs'
&& isFeatureSupported(esmLoadReadFile)
&& loaded.responseURL?.startsWith('file:') // Could be data:
&& !filePath.endsWith('.cjs') // CJS syntax doesn't need to be transformed for interop
) {
const code = await readFile(new URL(url), 'utf8');
// if the file extension is .js, only transform if using esm syntax
if (!filePath.endsWith('.js') || isESM(code)) {
/**
* es or cjs module lexer unfortunately cannot be used because it doesn't support
* typescript syntax
*
* While the full code is transformed, only the exports are used for parsing.
* In fact, the code can't even run because imports cannot be resolved relative
* from the data: URL.
*
* This should pre-compile for the CJS loader to have a cache hit
*
* I considered extracting the CJS exports from esbuild via (0&&(module.exports={})
* to minimize the data URL size but this only works for ESM->CJS and not CTS files
* which are already in CJS syntax.
* In CTS, module.exports can be written in any pattern.
*/
const transformed = transformSync(
code,
filePath,
{
tsconfigRaw: fileMatcher?.(filePath) as TransformOptions['tsconfigRaw'],
},
);
const filePathWithNamespace = urlNamespace ? `${filePath}?namespace=${encodeURIComponent(urlNamespace)}` : filePath;
loaded.responseURL = `data:text/javascript,${encodeURIComponent(transformed.code)}?filePath=${encodeURIComponent(filePathWithNamespace)}`;
log(3, 'returning CJS export annotation', loaded);
return loaded;
}
}
// CommonJS and Internal modules (e.g. node:*)
if (!loaded.source) {
return loaded;
}
const code = loaded.source.toString();
if (
// Support named imports in JSON modules
loaded.format === 'json'
|| tsExtensionsPattern.test(url)
) {
const transformed = await transform(
code,
filePath,
{
tsconfigRaw: (
path.isAbsolute(filePath)
? fileMatcher?.(filePath) as TransformOptions['tsconfigRaw']
: undefined
),
},
);
return {
format: 'module',
source: inlineSourceMap(transformed),
};
}
if (loaded.format === 'module') {
const dynamicImportTransformed = transformDynamicImport(filePath, code);
if (dynamicImportTransformed) {
loaded.source = inlineSourceMap(dynamicImportTransformed);
}
}
return loaded;
};
if (debugEnabled) {
const originalLoad = load;
load = async (
url,
context,
nextLoad,
) => {
log(2, 'load', {
url,
context,
});
const result = await originalLoad(url, context, nextLoad);
log(1, 'loaded', {
url,
result,
});
return result;
};
}
export { load };
================================================
FILE: src/esm/hook/package-json.ts
================================================
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import type { PackageJson } from 'type-fest';
const packageJsonCache = new Map<string, PackageJson | undefined>();
const readPackageJson = async (filePath: string) => {
if (packageJsonCache.has(filePath)) {
return packageJsonCache.get(filePath);
}
const exists = await fs.promises.access(filePath).then(
() => true,
() => false,
);
if (!exists) {
packageJsonCache.set(filePath, undefined);
return;
}
const packageJsonString = await fs.promises.readFile(filePath, 'utf8');
try {
const packageJson = JSON.parse(packageJsonString) as PackageJson;
packageJsonCache.set(filePath, packageJson);
return packageJson;
} catch {
throw new Error(`Error parsing: ${filePath}`);
}
};
// From Node.js
// https://github.com/nodejs/node/blob/e86a6383054623e5168384a83d8cd6ebfe1fb584/lib/internal/modules/esm/resolve.js#L229
const findPackageJson = async (
filePath: string,
) => {
let packageJsonUrl = new URL('package.json', filePath);
while (true) {
// Don't look outside of /node_modules/
if (packageJsonUrl.pathname.endsWith('/node_modules/package.json')) {
break;
}
const packageJsonPath = fileURLToPath(packageJsonUrl);
const packageJson = await readPackageJson(packageJsonPath);
if (packageJson) {
return packageJson;
}
const lastPackageJSONUrl = packageJsonUrl;
packageJsonUrl = new URL('../package.json', packageJsonUrl);
// Terminates at root where ../package.json equals ../../package.json
// (can't just check "/package.json" for Windows support).
if (packageJsonUrl.pathname === lastPackageJSONUrl.pathname) {
break;
}
}
};
export const getPackageType = async (
filePath: string,
) => {
const packageJson = await findPackageJson(filePath);
return packageJson?.type ?? 'commonjs';
};
================================================
FILE: src/esm/hook/resolve.ts
================================================
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import type {
ResolveHook,
ResolveHookContext,
} from 'node:module';
import type { PackageJson } from 'type-fest';
import { readJsonFile } from '../../utils/read-json-file.js';
import { mapTsExtensions } from '../../utils/map-ts-extensions.js';
import type { NodeError } from '../../types.js';
import { tsconfigPathsMatcher, allowJs } from '../../utils/tsconfig.js';
import {
requestAcceptsQuery,
fileUrlPrefix,
tsExtensionsPattern,
isDirectoryPattern,
isRelativePath,
} from '../../utils/path-utils.js';
import type { TsxRequest } from '../types.js';
import { logEsm as log, debugEnabled } from '../../utils/debug.js';
import {
getFormatFromFileUrl,
namespaceQuery,
getNamespace,
} from './utils.js';
import { data } from './initialize.js';
type NextResolve = Parameters<ResolveHook>[2];
const getMissingPathFromNotFound = (
nodeError: NodeError,
) => {
if (nodeError.url) {
return nodeError.url;
}
const isExportPath = nodeError.message.match(/^Cannot find module '([^']+)'/);
if (isExportPath) {
const [, exportPath] = isExportPath;
return exportPath;
}
const isPackagePath = nodeError.message.match(/^Cannot find package '([^']+)'/);
if (isPackagePath) {
const [, packagePath] = isPackagePath;
if (!path.isAbsolute(packagePath)) {
return;
}
const packageUrl = pathToFileURL(packagePath);
// Node v20.0.0 logs the package directory
// Slash check / works on Windows as well because it's a path URL
if (packageUrl.pathname.endsWith('/')) {
packageUrl.pathname += 'package.json';
}
// Node v21+ logs the package package.json path
if (packageUrl.pathname.endsWith('/package.json')) {
// packageJsonUrl.pathname += '/package.json';
const packageJson = readJsonFile<PackageJson>(packageUrl);
if (packageJson?.main) {
return new URL(packageJson.main, packageUrl).toString();
}
} else {
// Node v22.6.0 logs the entry path so we don't need to look it up from package.json
return packageUrl.toString();
}
}
};
const resolveExtensions = async (
url: string,
context: ResolveHookContext,
nextResolve: NextResolve,
throwError?: boolean,
) => {
const tryPaths = mapTsExtensions(url);
log(3, 'resolveExtensions', {
url,
context,
throwError,
tryPaths,
});
if (!tryPaths) {
return;
}
let caughtError: unknown;
for (const tsPath of tryPaths) {
try {
return await nextResolve(tsPath, context);
} catch (error) {
const { code } = error as NodeError;
if (
code !== 'ERR_MODULE_NOT_FOUND'
&& code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
) {
throw error;
}
caughtError = error;
}
}
if (throwError) {
throw caughtError;
}
};
const resolveBase: ResolveHook = async (
specifier,
context,
nextResolve,
) => {
log(3, 'resolveBase', {
specifier,
context,
specifierStartsWithFileUrl: specifier.startsWith(fileUrlPrefix),
isRelativePath: isRelativePath(specifier),
tsExtensionsPattern: tsExtensionsPattern.test(context.parentURL!),
allowJs,
});
/**
* Only prioritize TypeScript extensions for file paths (no dependencies)
* TS aliases are pre-resolved so they're file paths
*
* If `allowJs` is set in `tsconfig.json`, then we'll apply the same resolution logic
* to files without a TypeScript extension.
*/
if (
(
specifier.startsWith(fileUrlPrefix)
|| isRelativePath(specifier)
) && (
tsExtensionsPattern.test(context.parentURL!)
|| allowJs
)
) {
const resolved = await resolveExtensions(specifier, context, nextResolve);
log(3, 'resolveBase resolved', {
specifier,
context,
resolved,
});
if (resolved) {
return resolved;
}
}
try {
return await nextResolve(specifier, context);
} catch (error) {
log(3, 'resolveBase error', {
specifier,
context,
error,
});
if (error instanceof Error) {
const nodeError = error as NodeError;
if (nodeError.code === 'ERR_MODULE_NOT_FOUND') {
// Resolving .js -> .ts in exports/imports map
const errorPath = getMissingPathFromNotFound(nodeError);
if (errorPath) {
const resolved = await resolveExtensions(errorPath, context, nextResolve);
if (resolved) {
return resolved;
}
}
}
}
throw error;
}
};
const resolveDirectory: ResolveHook = async (
specifier,
context,
nextResolve,
) => {
log(3, 'resolveDirectory', {
specifier,
context,
isDirectory: isDirectoryPattern.test(specifier),
});
if (specifier === '.' || specifier === '..' || specifier.endsWith('/..')) {
specifier += '/';
}
if (isDirectoryPattern.test(specifier)) {
const urlParsed = new URL(specifier, context.parentURL);
// If directory, can be index.js, index.ts, etc.
urlParsed.pathname = path.join(urlParsed.pathname, 'index');
return (await resolveExtensions(
urlParsed.toString(),
context,
nextResolve,
true,
))!;
}
try {
return await resolveBase(specifier, context, nextResolve);
} catch (error) {
if (error instanceof Error) {
log(3, 'resolveDirectory error', {
specifier,
context,
error,
});
const nodeError = error as NodeError;
if (nodeError.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
const errorPath = getMissingPathFromNotFound(nodeError);
if (errorPath) {
try {
return (await resolveExtensions(
`${errorPath}/index`,
context,
nextResolve,
true,
))!;
} catch (_error) {
const __error = _error as Error;
const { message } = __error;
__error.message = __error.message.replace(`${'/index'.replace('/', path.sep)}'`, "'");
__error.stack = __error.stack!.replace(message, __error.message);
throw __error;
}
}
}
}
throw error;
}
};
const resolveTsPaths: ResolveHook = async (
specifier,
context,
nextResolve,
) => {
log(3, 'resolveTsPaths', {
specifier,
context,
requestAcceptsQuery: requestAcceptsQuery(specifier),
tsconfigPathsMatcher,
fromNodeModules: context.parentURL?.includes('/node_modules/'),
});
if (
// Bare specifier
!requestAcceptsQuery(specifier)
// TS path alias
&& tsconfigPathsMatcher
&& !context.parentURL?.includes('/node_modules/')
) {
const possiblePaths = tsconfigPathsMatcher(specifier);
log(3, 'resolveTsPaths', {
possiblePaths,
});
for (const possiblePath of possiblePaths) {
try {
return await resolveDirectory(
pathToFileURL(possiblePath).toString(),
context,
nextResolve,
);
} catch {}
}
}
return resolveDirectory(specifier, context, nextResolve);
};
const tsxProtocol = 'tsx://';
// eslint-disable-next-line import-x/no-mutable-exports
let resolve: ResolveHook = async (
specifier,
context,
nextResolve,
) => {
if (!data.active || specifier.startsWith('node:')) {
return nextResolve(specifier, context);
}
let requestNamespace = getNamespace(specifier) ?? (
// Inherit namespace from parent
context.parentURL && getNamespace(context.parentURL)
);
if (data.namespace) {
let tsImportRequest: TsxRequest | undefined;
// Initial request from tsImport()
if (specifier.startsWith(tsxProtocol)) {
try {
tsImportRequest = JSON.parse(specifier.slice(tsxProtocol.length));
} catch {}
if (tsImportRequest?.namespace) {
requestNamespace = tsImportRequest.namespace;
}
}
if (data.namespace !== requestNamespace) {
return nextResolve(specifier, context);
}
if (tsImportRequest) {
specifier = tsImportRequest.specifier;
context.parentURL = tsImportRequest.parentURL;
}
}
const [cleanSpecifier, query] = specifier.split('?');
const resolved = await resolveTsPaths(
cleanSpecifier,
context,
nextResolve,
);
log(2, 'nextResolve', {
resolved,
});
if (resolved.format === 'builtin') {
return resolved;
}
// For TypeScript extensions that Node can't detect the format of
if (
(
!resolved.format
|| resolved.format === 'commonjs-typescript'
|| resolved.format === 'module-typescript'
)
// Filter out data: (sourcemaps)
&& resolved.url.startsWith(fileUrlPrefix)
) {
resolved.format = await getFormatFromFileUrl(resolved.url);
log(2, 'getFormatFromFileUrl', {
resolved,
format: resolved.format,
});
}
if (query) {
resolved.url += `?${query}`;
}
// Inherit namespace
if (
requestNamespace
&& !resolved.url.includes(namespaceQuery)
) {
resolved.url += (resolved.url.includes('?') ? '&' : '?') + namespaceQuery + requestNamespace;
}
return resolved;
};
if (debugEnabled) {
const originalResolve = resolve;
resolve = async (
specifier,
context,
nextResolve,
) => {
log(2, 'resolve', {
specifier,
context,
});
const result = await originalResolve(specifier, context, nextResolve);
log(1, 'resolved', {
specifier,
context,
result,
});
return result;
};
}
export { resolve };
================================================
FILE: src/esm/hook/utils.ts
================================================
import path from 'node:path';
import { tsExtensions } from '../../utils/path-utils.js';
import { getPackageType } from './package-json.js';
export const getFormatFromFileUrl = (fileUrl: string) => {
const { pathname } = new URL(fileUrl);
const extension = path.extname(pathname);
if (extension === '.mts' || extension === '.mjs') {
return 'module';
}
if (extension === '.cts' || extension === '.cjs') {
return 'commonjs';
}
if (extension === '.js' || tsExtensions.includes(extension)) {
return getPackageType(fileUrl);
}
};
export const namespaceQuery = 'tsx-namespace=';
export const getNamespace = (
url: string,
) => {
const index = url.indexOf(namespaceQuery);
if (index === -1) {
return;
}
const charBefore = url[index - 1];
if (charBefore !== '?' && charBefore !== '&') {
return;
}
const startIndex = index + namespaceQuery.length;
const endIndex = url.indexOf('&', startIndex);
return (
endIndex === -1
? url.slice(startIndex)
: url.slice(startIndex, endIndex)
);
};
================================================
FILE: src/esm/index.ts
================================================
import { isMainThread } from 'node:worker_threads';
import { isFeatureSupported, moduleRegister } from '../utils/node-features.js';
import { register } from './api/index.js';
// Loaded via --import flag
if (
isFeatureSupported(moduleRegister)
&& isMainThread
) {
register();
}
export * from './hook/index.js';
================================================
FILE: src/esm/types.ts
================================================
export type Message = {
type: 'deactivated';
} | {
type: 'load';
url: string;
};
export type TsxRequest = {
namespace: string;
parentURL: string;
specifier: string;
};
================================================
FILE: src/loader.ts
================================================
// Hook require() to transform to CJS
// eslint-disable-next-line import-x/no-unresolved, @typescript-eslint/no-require-imports
require('./cjs/index.cjs');
/*
Hook import/import() to transform to ESM
Can be used in Node v12 to support dynamic `import()`
*/
export * from './esm/index.js';
================================================
FILE: src/patch-repl.ts
================================================
import repl, { type REPLServer, type REPLEval } from 'node:repl';
import { transform } from 'esbuild';
const patchEval = (nodeRepl: REPLServer) => {
const { eval: defaultEval } = nodeRepl;
const preEval: REPLEval = async function (code, context, filename, callback) {
try {
const transformed = await transform(
code,
{
sourcefile: filename,
loader: 'ts',
tsconfigRaw: {
compilerOptions: {
preserveValueImports: true,
},
},
define: {
require: 'global.require',
},
},
);
code = transformed.code;
} catch {}
return defaultEval.call(this, code, context, filename, callback);
};
// @ts-expect-error overwriting read-only property
nodeRepl.eval = preEval;
};
const { start } = repl;
repl.start = function () {
const nodeRepl = Reflect.apply(start, this, arguments);
patchEval(nodeRepl);
return nodeRepl;
};
================================================
FILE: src/preflight.cts
================================================
import { constants as osConstants } from 'node:os';
import { isMainThread } from 'node:worker_threads';
import { connectingToServer } from './utils/ipc/client.js';
import './suppress-warnings.cjs';
type BaseEventListener = () => void;
const bindHiddenSignalsHandler = (
signals: NodeJS.Signals[],
handler: NodeJS.SignalsListener,
) => {
type RelaySignals = typeof signals[number];
const hiddenHandlers = new Map<RelaySignals, NodeJS.SignalsListener>();
for (const signal of signals) {
const hiddenHandler = (receivedSignal: NodeJS.Signals) => {
handler(receivedSignal);
/**
* Since we're setting a custom signal handler, we need to emulate the
* default behavior when there are no other handlers set
*/
if (process.listenerCount(signal) === 0) {
// eslint-disable-next-line n/no-process-exit
process.exit(128 + osConstants.signals[signal]);
}
};
process.on(signal, hiddenHandler);
hiddenHandlers.set(signal, hiddenHandler);
}
/**
* Hide relaySignal from process.listeners() and process.listenerCount()
*/
const { listenerCount, listeners } = process;
process.listenerCount = function (eventName) {
let count = Reflect.apply(listenerCount, this, arguments);
if (signals.includes(eventName as RelaySignals)) {
count -= 1;
}
return count;
};
process.listeners = function (eventName) {
const result: BaseEventListener[] = Reflect.apply(listeners, this, arguments);
if (signals.includes(eventName as RelaySignals)) {
return result.filter(
listener => listener !== hiddenHandlers.get(eventName as RelaySignals),
);
}
return result;
};
};
/**
* Seems module.register() calls the loader with the same Node arguments
* which causes this preflight to be loaded in the loader thread
*/
if (isMainThread) {
/**
* Hook require() to transform to CJS
*
* This needs to be loaded via --require flag so subsequent --require
* flags can support TypeScript.
*
* This is also added in loader.ts for the loader API.
* Although it is required twice, it's not executed twice because
* it's cached.
*/
// eslint-disable-next-line import-x/no-unresolved, @typescript-eslint/no-require-imports
require('./cjs/index.cjs');
(async () => {
const sendToParent = await connectingToServer;
if (sendToParent) {
bindHiddenSignalsHandler(['SIGINT', 'SIGTERM'], (signal: NodeJS.Signals) => {
sendToParent({
type: 'signal',
signal,
});
});
}
})();
}
================================================
FILE: src/remove-argv-flags.ts
================================================
import {
typeFlag,
type Flags,
type TypeFlagOptions,
} from 'type-flag';
export const ignoreAfterArgument = (
ignoreFirstArgument = true, // Used for watch
): TypeFlagOptions['ignore'] => {
let ignore = false;
return (type) => {
if (
ignore
|| type === 'unknown-flag'
) {
return true;
}
if (type === 'argument') {
ignore = true;
return ignoreFirstArgument;
}
};
};
export const removeArgvFlags = (
tsxFlags: Flags,
argv = process.argv.slice(2),
) => {
typeFlag(
tsxFlags,
argv,
{
ignore: ignoreAfterArgument(),
},
);
return argv;
};
================================================
FILE: src/repl.ts
================================================
// Deprecated: Delete entry-point in next major in favor of patch-repl.ts
import repl, { type REPLEval } from 'node:repl';
import { version } from '../package.json';
import { transform } from './utils/transform/index.js';
// Copied from
// https://github.com/nodejs/node/blob/v18.2.0/lib/internal/main/repl.js#L37
console.log(
`Welcome to tsx v${version} (Node.js ${process.version}).\n`
+ 'Type ".help" for more information.',
);
const nodeRepl = repl.start();
const { eval: defaultEval } = nodeRepl;
const preEval: REPLEval = async function (code, context, filename, callback) {
const transformed = await transform(
code,
filename,
{
loader: 'ts',
tsconfigRaw: {
compilerOptions: {
preserveValueImports: true,
},
},
define: {
require: 'global.require',
},
},
).catch(
(error) => {
console.log(error.message);
return { code: '\n' };
},
);
return defaultEval.call(this, transformed.code, context, filename, callback);
};
// @ts-expect-error overriding read-only property
nodeRepl.eval = preEval;
================================================
FILE: src/run.ts
================================================
import type { StdioOptions } from 'node:child_process';
import { pathToFileURL } from 'node:url';
import spawn from 'cross-spawn';
import { isFeatureSupported, moduleRegister } from './utils/node-features.js';
export const run = (
argv: string[],
options?: {
noCache?: boolean;
tsconfigPath?: string;
ipc?: boolean;
},
) => {
const environment = { ...process.env };
const stdio: StdioOptions = [
'inherit', // stdin
'inherit', // stdout
'inherit', // stderr
];
// If parent process spawns tsx with ipc, spawn child with ipc
if (process.send) {
stdio.push('ipc');
}
if (options) {
if (options.noCache) {
environment.TSX_DISABLE_CACHE = '1';
}
if (options.tsconfigPath) {
environment.TSX_TSCONFIG_PATH = options.tsconfigPath;
}
}
const shouldPatchRepl = argv.filter(flag => (flag !== '-i' && flag !== '--interactive')).length === 0;
return spawn(
process.execPath,
[
'--require',
require.resolve('./preflight.cjs'),
...(
shouldPatchRepl
? [
'--require',
require.resolve('./patch-repl.cjs'),
]
: []
),
isFeatureSupported(moduleRegister) ? '--import' : '--loader',
pathToFileURL(require.resolve('./loader.mjs')).toString(),
...argv,
],
{
stdio,
env: environment,
},
);
};
================================================
FILE: src/source-map.ts
================================================
import type { Transformed } from './utils/transform/apply-transformers.js';
const inlineSourceMapPrefix = '\n//# sourceMappingURL=data:application/json;base64,';
// TODO: Build this logic into inlineSourceMap
// If undefined, assume sourcemap is enabled
export const shouldApplySourceMap = () => process.sourceMapsEnabled ?? true;
export const inlineSourceMap = (
{ code, map }: Transformed,
) => (
code
+ inlineSourceMapPrefix
+ Buffer.from(JSON.stringify(map), 'utf8').toString('base64')
);
================================================
FILE: src/suppress-warnings.cts
================================================
// Deprecated: Move to preflight.cts & delete entry-point in next major
const ignoreWarnings = new Set([
// v18.0.0
'Custom ESM Loaders is an experimental feature. This feature could change at any time',
// Changed in Node v18.13.0 via https://github.com/nodejs/node/pull/45424
'Custom ESM Loaders is an experimental feature and might change at any time',
// For JSON modules via https://github.com/nodejs/node/pull/46901
'Import assertions are not a stable feature of the JavaScript language. Avoid relying on their current behavior and syntax as those might change in a future version of Node.js.',
]);
const { emit } = process;
// @ts-expect-error emit type mismatch
process.emit = function (event: 'warning', warning: Error) {
if (
event === 'warning'
&& ignoreWarnings.has(warning.message)
) {
return;
}
return Reflect.apply(emit, this, arguments);
};
================================================
FILE: src/types.ts
================================================
export type NodeError = Error & {
code: string;
url?: string;
path?: string;
};
export type RequiredProperty<Type, Keys extends keyof Type> = Type & { [P in Keys]-?: Type[P] };
================================================
FILE: src/utils/debug.ts
================================================
import { inspect } from 'node:util';
import { writeSync } from 'node:fs';
import {
options, bgBlue, black, bgLightYellow, bgGray,
} from 'kolorist';
export const debugEnabled = Number(process.env.TSX_DEBUG);
// Force colors in debug mode
if (debugEnabled) {
options.enabled = true;
options.supportLevel = 3;
}
const createLog = (
name: string,
) => (
level: number,
...args: any[]
) => {
if (!debugEnabled) {
return;
}
if (level > debugEnabled) {
return;
}
const prefix = `${bgGray(` tsx P${process.pid} `)} ${name}`;
const logMessage = args.map(argumentElement => (
typeof argumentElement === 'string'
? argumentElement
: inspect(argumentElement, { colors: true })
)).join(' ');
writeSync(
1,
`${prefix} ${logMessage}\n`,
);
};
export const logCjs = createLog(bgLightYellow(black(' CJS ')));
export const logEsm = createLog(bgBlue(' ESM '));
export const time = <T extends (...args: any[]) => unknown>(
name: string,
_function: T,
threshold = 100,
): T => function (
this: unknown,
...args: Parameters<T>
) {
const timeStart = Date.now();
const logTimeElapsed = () => {
const elapsed = Date.now() - timeStart;
if (elapsed > threshold) {
console.log(name, {
args,
elapsed,
});
}
};
const result = Reflect.apply(_function, this, args);
if (
result
&& typeof result === 'object'
&& 'then' in result
) {
(result as Promise<unknown>).then(
logTimeElapsed,
// Ignore error in this chain
() => {},
);
} else {
logTimeElapsed();
}
return result;
} as T;
================================================
FILE: src/utils/es-module-lexer.ts
================================================
import { parse as parseJs } from 'es-module-lexer/js';
let parseWasm: typeof import('es-module-lexer').parse | undefined;
// When Node's --jitless flag is set, WebAssembly is not available
if (typeof WebAssembly !== 'undefined') {
(async () => {
const { parse, init } = await import('es-module-lexer');
await init;
parseWasm = parse;
})();
}
export const parseEsm = (
code: string,
filename?: string,
) => (
parseWasm
? parseWasm(code, filename)
: parseJs(code, filename)
);
/*
Previously, this regex was used as a naive ESM catch,
but turns out regex is slower than the lexer so removing
it made the check faster.
Catches:
import a from 'b'
import 'b';
import('b');
export{a};
export default a;
Doesn't catch:
EXPORT{a}
exports.a = 1
module.exports = 1
const esmPattern = /\b(?:import|export)\b/;
*/
export const isESM = (code: string) => {
if (!code.includes('import') && !code.includes('export')) {
return false;
}
try {
const hasModuleSyntax = parseEsm(code)[3];
return hasModuleSyntax;
} catch {
/**
* If it fails to parse, there's a syntax error
* Let esbuild handle it for better error messages
*/
return true;
}
};
================================================
FILE: src/utils/ipc/client.ts
================================================
import net from 'node:net';
import { getPipePath } from './get-pipe-path.js';
export type SendToParent = (data: Record<string, unknown>) => void;
export type Parent = {
send: SendToParent | void;
};
const connectToServer = () => new Promise<SendToParent | void>((resolve) => {
const pipePath = getPipePath(process.ppid);
const socket: net.Socket = net.createConnection(
pipePath,
() => {
const sendToParent: SendToParent = (data) => {
const messageBuffer = Buffer.from(JSON.stringify(data));
const lengthBuffer = Buffer.alloc(4);
lengthBuffer.writeInt32BE(messageBuffer.length, 0);
socket.write(Buffer.concat([lengthBuffer, messageBuffer]));
};
resolve(sendToParent);
},
);
/**
* Ignore error when:
* - Called as a loader and there is no server
* - Nested process when using --test and the ppid is incorrect
*/
socket.on('error', () => {
resolve();
});
// Prevent Node from waiting for this socket to close before exiting
socket.unref();
});
export const parent: Parent = {
send: undefined,
};
export const connectingToServer = connectToServer();
connectingToServer.then(
(send) => {
parent.send = send;
},
() => {},
);
================================================
FILE: src/utils/ipc/get-pipe-path.ts
================================================
import path from 'node:path';
import { tmpdir } from '../temporary-directory.js';
import { isWindows } from '../is-windows.js';
export const getPipePath = (processId: number) => {
const pipePath = path.join(tmpdir, `${processId}.pipe`);
return (
isWindows
? `\\\\?\\pipe\\${pipePath}`
: pipePath
);
};
================================================
FILE: src/utils/ipc/server.ts
================================================
import net from 'node:net';
import fs from 'node:fs';
import { tmpdir } from '../temporary-directory.js';
import { isWindows } from '../is-windows.js';
import { getPipePath } from './get-pipe-path.js';
type OnMessage = (message: Buffer) => void;
const bufferData = (
onMessage: OnMessage,
) => {
let buffer = Buffer.alloc(0);
return (data: Buffer) => {
buffer = Buffer.concat([buffer, data]);
while (buffer.length > 4) {
const messageLength = buffer.readInt32BE(0);
if (buffer.length >= 4 + messageLength) {
const message = buffer.slice(4, 4 + messageLength);
onMessage(message);
buffer = buffer.slice(4 + messageLength);
} else {
break;
}
}
};
};
export const createIpcServer = async () => {
const server = net.createServer((socket) => {
socket.on('data', bufferData((message: Buffer) => {
const data = JSON.parse(message.toString());
server.emit('data', data);
}));
});
const pipePath = getPipePath(process.pid);
await fs.promises.mkdir(tmpdir, { recursive: true });
/**
* Fix #457 (https://github.com/privatenumber/tsx/issues/457)
*
* Avoid the error "EADDRINUSE: address already in use"
*
* If the pipe file already exists, it means that the previous process has been closed abnormally.
*
* We can safely delete the pipe file, the previous process must has been closed,
* as pid is unique at the same.
*/
await fs.promises.rm(pipePath, {
force: true,
});
await new Promise<void>((resolve, reject) => {
server.listen(pipePath, resolve);
server.on('error', reject);
});
// Prevent Node from waiting for this socket to close before exiting
server.unref();
process.on('exit', () => {
server.close();
/**
* Only clean on Unix
*
* https://nodejs.org/api/net.html#ipc-support:
* On Windows, the local domain is implemented using a named pipe.
* The path must refer to an entry in \\?\pipe\ or \\.\pipe\.
* Any characters are permitted, but the latter may do some processing
* of pipe names, such as resolving .. sequences. Despite how it might
* look, the pipe namespace is flat. Pipes will not persist. They are
* removed when the last reference to them is closed. Unlike Unix domain
* sockets, Windows will close and remove the pipe when the owning process exits.
*/
if (!isWindows) {
try {
fs.rmSync(pipePath);
} catch {}
}
});
return server;
};
================================================
FILE: src/utils/is-windows.ts
================================================
export const isWindows = process.platform === 'win32';
================================================
FILE: src/utils/map-ts-extensions.ts
================================================
import path from 'node:path';
import { isFilePath, fileUrlPrefix, nodeModulesPath } from './path-utils.js';
const implicitJsExtensions = ['.js', '.json'];
const implicitTsExtensions = ['.ts', '.tsx', '.jsx'];
// Guess extension
const localExtensions = [...implicitTsExtensions, ...implicitJsExtensions];
/**
* If dependency, prioritize .js extensions over .ts
*
* .js is more likely to behave correctly than the .ts file
* https://github.com/evanw/esbuild/releases/tag/v0.20.0
*/
const dependencyExtensions = [...implicitJsExtensions, ...implicitTsExtensions];
// Swap extension
const tsExtensions: Record<string, string[]> = Object.create(null);
tsExtensions['.js'] = ['.ts', '.tsx', '.js', '.jsx'];
tsExtensions['.jsx'] = ['.tsx', '.ts', '.jsx', '.js'];
tsExtensions['.cjs'] = ['.cts'];
tsExtensions['.mjs'] = ['.mts'];
export const mapTsExtensions = (
filePath: string,
) => {
const splitPath = filePath.split('?');
const pathQuery = splitPath[1] ? `?${splitPath[1]}` : '';
const [pathname] = splitPath;
const extension = path.extname(pathname);
const tryPaths: string[] = [];
const tryExtensions = tsExtensions[extension];
if (tryExtensions) {
const extensionlessPath = pathname.slice(0, -extension.length);
tryPaths.push(
...tryExtensions.map(
extension_ => (
extensionlessPath
+ extension_
+ pathQuery
),
),
);
}
const guessExtensions = (
(
!(filePath.startsWith(fileUrlPrefix) || isFilePath(pathname))
|| pathname.includes(nodeModulesPath)
|| pathname.includes('/node_modules/') // For file:// URLs on Windows
)
? dependencyExtensions
: localExtensions
);
tryPaths.push(
...guessExtensions.map(
extension_ => (
pathname
+ extension_
+ pathQuery
),
),
);
return tryPaths;
};
================================================
FILE: src/utils/node-features.ts
================================================
export type Version = [number, number, number];
// Is v1 greater or equal to v2?
const isVersionGreaterOrEqual = (v1: Version, v2: Version): boolean => {
const majorDiff = v1[0] - v2[0];
if (majorDiff === 0) {
const minorDiff = v1[1] - v2[1];
if (minorDiff === 0) {
return v1[2] >= v2[2];
}
return minorDiff > 0;
}
return majorDiff > 0;
};
const currentNodeVersion = process.versions.node.split('.').map(Number) as Version;
export const isFeatureSupported = (
versions: Version[],
current = currentNodeVersion,
) => {
for (let i = 0; i < versions.length; i += 1) {
const version = versions[i];
// If last version, check if greater
if (i === versions.length - 1) {
return isVersionGreaterOrEqual(current, version);
}
// Otherwise, check within major range
if (current[0] === version[0]) {
return isVersionGreaterOrEqual(current, version);
}
}
return false;
};
// https://nodejs.org/docs/latest/api/module.html#moduleregisterspecifier-parenturl-options
export const moduleRegister: Version[] = [
[18, 19, 0],
[20, 6, 0],
];
// https://nodejs.org/docs/latest/api/esm.html#import-attributes
export const importAttributes: Version[] = [
[18, 19, 0],
[20, 10, 0],
[21, 0, 0],
];
// https://github.com/nodejs/node/releases/tag/v21.0.0
export const testRunnerGlob: Version[] = [
[21, 0, 0],
];
// https://github.com/nodejs/node/pull/50825
export const esmLoadReadFile: Version[] = [
[20, 11, 0],
[21, 3, 0],
];
// https://github.com/nodejs/node/pull/55085
export const requireEsm: Version[] = [
[20, 19, 0],
[23, 0, 0],
];
================================================
FILE: src/utils/path-utils.ts
================================================
import path from 'node:path';
/**
* Prior to calling this function, it's expected that Windows paths have been filtered out
* via path.isAbsolute()
*
* Windows paths cannot be correctly parsed (e.g. new URL('C:\Users\Example\file.txt')
*/
const getScheme = (url: string) => {
const schemeIndex = url.indexOf(':');
if (schemeIndex === -1) { return; }
return url.slice(0, schemeIndex);
};
export const isRelativePath = (request: string) => (
request[0] === '.'
&& (
request[1] === '/'
|| (request[1] === '.' || request[2] === '/')
)
);
export const isFilePath = (request: string) => (
isRelativePath(request)
|| path.isAbsolute(request)
);
// In Node, bare specifiers (packages and core modules) do not accept queries
export const requestAcceptsQuery = (request: string) => {
// ./foo.js?query
// /foo.js?query in UNIX
if (isFilePath(request)) {
return true;
}
const scheme = getScheme(request);
return (
// Expected to be file, https, etc...
scheme
// node:url maps to a bare-specifier, which does not accept queries
// But URLs like file:// or https:// do
&& scheme !== 'node'
);
};
export const fileUrlPrefix = 'file://';
export const tsExtensions = ['.ts', '.tsx', '.jsx', '.mts', '.cts'];
export const tsExtensionsPattern = /\.([cm]?ts|[tj]sx)($|\?)/;
export const cjsExtensionPattern = /[/\\].+\.(?:cts|cjs)(?:$|\?)/;
export const isJsonPattern = /\.json($|\?)/;
export const isDirectoryPattern = /\/(?:$|\?)/;
// Only matches packages names without subpaths (e.g. `foo` but not `foo/bar`)
// Back slash included to exclude Windows paths
export const isBarePackageNamePattern = /^(?:@[^/]+\/)?[^/\\]+$/;
export const nodeModulesPath = `${path.sep}node_modules${path.sep}`;
================================================
FILE: src/utils/read-json-file.ts
================================================
import fs from 'node:fs';
export const readJsonFile = <JsonType>(
filePath: string | URL,
) => {
try {
const jsonString = fs.readFileSync(filePath, 'utf8');
return JSON.parse(jsonString) as JsonType;
} catch {}
};
================================================
FILE: src/utils/sha1.ts
================================================
import crypto from 'node:crypto';
export const sha1 = (data: string) => (
crypto
.createHash('sha1')
.update(data)
.digest('hex')
);
================================================
FILE: src/utils/temporary-directory.ts
================================================
import path from 'node:path';
import os from 'node:os';
/**
* Cache directory is based on the user's identifier
* to avoid permission issues when accessed by a different user
*/
const { geteuid } = process;
const userId = (
geteuid
// For Linux users with virtual users on CI (e.g. Docker)
? geteuid()
// Use username on Windows because it doesn't have id
: os.userInfo().username
);
/**
* This ensures that the cache directory is unique per user
* and has the appropriate permissions
*/
export const tmpdir = path.join(os.tmpdir(), `tsx-${userId}`);
================================================
FILE: src/utils/transform/apply-transformers.ts
================================================
import remapping, { type SourceMap, type SourceMapInput } from '@ampproject/remapping';
type MaybePromise<T> = T | Promise<T>;
type TransformerResult = {
code: string;
map: SourceMap;
} | undefined;
type Transformer<
ReturnType extends MaybePromise<TransformerResult>
> = (
filePath: string,
code: string,
) => ReturnType;
export type Transformed = {
code: string;
map: SourceMap;
};
export const applyTransformersSync = (
filePath: string,
code: string,
transformers: Transformer<TransformerResult>[],
): Transformed => {
const maps: SourceMap[] = [];
const result = { code };
for (const transformer of transformers) {
const transformed = transformer(filePath, result.code);
if (transformed) {
Object.assign(result, transformed);
maps.unshift(transformed.map);
}
}
return {
...result,
map: remapping(maps as SourceMapInput[], () => null),
};
};
export const applyTransformers = async (
filePath: string,
code: string,
transformers: Transformer<MaybePromise<TransformerResult>>[],
): Promise<Transformed> => {
const maps: SourceMap[] = [];
const result = { code };
for (const transformer of transformers) {
const transformed = await transformer(filePath, result.code);
if (transformed) {
Object.assign(result, transformed);
maps.unshift(transformed.map);
}
}
return {
...result,
map: remapping(maps as SourceMapInput[], () => null),
};
};
================================================
FILE: src/utils/transform/cache.ts
================================================
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { readJsonFile } from '../read-json-file.js';
import { tmpdir } from '../temporary-directory.js';
import type { Transformed } from './apply-transformers.js';
const noop = () => {};
const getTime = () => Math.floor(Date.now() / 1e8);
class FileCache<ReturnType> extends Map<string, ReturnType> {
/**
* By using tmpdir, the expectation is for the OS to clean any files
* that haven't been read for a while.
*
* macOS - 3 days: https://superuser.com/a/187105
* Linux - https://serverfault.com/a/377349
*
* Note on Windows, temp files are not cleaned up automatically.
* https://superuser.com/a/1599897
*/
cacheDirectory = tmpdir;
// Maintained so we can remove it on Windows
oldCacheDirectory = path.join(os.tmpdir(), 'tsx');
cacheFiles: {
time: number;
key: string;
fileName: string;
}[];
constructor() {
super();
// Handles race condition if multiple tsx instances are running (#22)
fs.mkdirSync(this.cacheDirectory, { recursive: true });
this.cacheFiles = fs.readdirSync(this.cacheDirectory).map((fileName) => {
const [time, key] = fileName.split('-');
return {
time: Number(time),
key,
fileName,
};
});
setImmediate(() => {
this.expireDiskCache();
this.removeOldCacheDirectory();
});
}
override get(key: string) {
const memoryCacheHit = super.get(key);
if (memoryCacheHit) {
return memoryCacheHit;
}
const diskCacheHit = this.cacheFiles.find(cache => cache.key === key);
if (!diskCacheHit) {
return;
}
const cacheFilePath = path.join(this.cacheDirectory, diskCacheHit.fileName);
const cachedResult = readJsonFile<ReturnType>(cacheFilePath);
if (!cachedResult) {
// Remove broken cache file
fs.promises.unlink(cacheFilePath).then(
() => {
const index = this.cacheFiles.indexOf(diskCacheHit);
this.cacheFiles.splice(index, 1);
},
() => {},
);
return;
}
// Load it into memory
super.set(key, cachedResult);
return cachedResult;
}
override set(key: string, value: ReturnType) {
super.set(key, value);
if (value) {
/**
* Time is inaccurate by ~27.7 hours to minimize data
* and because this level of fidelity wont matter
*/
const time = getTime();
fs.promises.writeFile(
path.join(this.cacheDirectory, `${time}-${key}`),
JSON.stringify(value),
).catch(noop);
}
return this;
}
expireDiskCache() {
const time = getTime();
for (const cache of this.cacheFiles) {
// Remove if older than ~7 days
if ((time - cache.time) > 7) {
fs.promises.unlink(path.join(this.cacheDirectory, cache.fileName)).catch(noop);
}
}
}
async removeOldCacheDirectory() {
try {
const exists = await fs.promises.access(this.oldCacheDirectory).then(() => true);
if (exists) {
if ('rm' in fs.promises) {
await fs.promises.rm(
this.oldCacheDirectory,
{
recursive: true,
force: true,
},
);
} else {
await fs.promises.rmdir(
this.oldCacheDirectory,
{ recursive: true },
);
}
}
} catch {}
}
}
export default (
process.env.TSX_DISABLE_CACHE
? new Map<string, Transformed>()
: new FileCache<Transformed>()
);
================================================
FILE: src/utils/transform/get-esbuild-options.ts
================================================
import path from 'node:path';
import type { TransformOptions, TransformResult } from 'esbuild';
import type { SourceMap } from '@ampproject/remapping';
export const baseConfig = Object.freeze({
target: `node${process.versions.node}`,
// "default" tells esbuild to infer loader from file name
// https://github.com/evanw/esbuild/blob/4a07b17adad23e40cbca7d2f8931e8fb81b47c33/internal/bundler/bundler.go#L158
loader: 'default',
});
// match Node.js debugger flags
// https://nodejs.org/api/cli.html#--inspecthostport
const NODE_DEBUGGER_FLAG_REGEX = /^--inspect(?:-brk|-port|-publish-uid|-wait)?(?:=|$)/;
const isNodeDebuggerEnabled = process.execArgv.some(flag => NODE_DEBUGGER_FLAG_REGEX.test(flag));
export const cacheConfig = {
...baseConfig,
sourcemap: true,
/**
* Improve performance by only generating sourcesContent
* when V8 coverage is enabled or Node.js debugger is enabled
*
* https://esbuild.github.io/api/#sources-content
*/
sourcesContent: Boolean(process.env.NODE_V8_COVERAGE) || isNodeDebuggerEnabled,
/**
* Smaller output for cache and marginal performance improvement:
* https://twitter.com/evanwallace/status/1396336348366180359?s=20
*
* minifyIdentifiers is disabled because debuggers don't use the
* `names` property from the source map
*
* minifySyntax is disabled because it does some tree-shaking
* eg. unused try-catch error variable
*/
minifyWhitespace: true,
/**
* esbuild renames variables even if minification is not enabled
* https://esbuild.github.io/try/#dAAwLjE5LjUAAGNvbnN0IGEgPSAxOwooZnVuY3Rpb24gYSgpIHt9KTs
*/
keepNames: true,
};
export const patchOptions = (
options: TransformOptions,
) => {
const originalSourcefile = options.sourcefile;
if (originalSourcefile) {
const extension = path.extname(originalSourcefile.split('?')[0]);
if (extension) {
// https://github.com/evanw/esbuild/issues/1932
if (extension === '.cts' || extension === '.mts') {
options.sourcefile = `${originalSourcefile.slice(0, -3)}ts`;
} else if (extension === '.mjs') { // only used by CJS loader
options.sourcefile = `${originalSourcefile.slice(0, -3)}js`;
}
} else {
// esbuild errors to detect loader when a file doesn't have an extension
options.sourcefile += '.js';
}
}
return (
result: TransformResult,
) => {
if (result.map) {
if (options.sourcefile !== originalSourcefile) {
result.map = result.map.replace(
JSON.stringify(options.sourcefile),
JSON.stringify(originalSourcefile),
);
}
result.map = JSON.parse(result.map);
}
return result as TransformResult & { map: SourceMap };
};
};
================================================
FILE: src/utils/transform/index.ts
================================================
import { pathToFileURL, fileURLToPath } from 'node:url';
import {
transform as esbuildTransform,
transformSync as esbuildTransformSync,
version as esbuildVersion,
type TransformOptions,
type TransformFailure,
} from 'esbuild';
import { sha1 } from '../sha1.js';
import {
version as transformDynamicImportVersion,
transformDynamicImport,
} from './transform-dynamic-import.js';
import cache from './cache.js';
import {
applyTransformersSync,
applyTransformers,
type Transformed,
} from './apply-transformers.js';
import {
cacheConfig,
patchOptions,
} from './get-esbuild-options.js';
const formatEsbuildError = (
error: TransformFailure,
) => {
error.name = 'TransformError';
// @ts-expect-error deleting non-option property
delete error.errors;
// @ts-expect-error deleting non-option property
delete error.warnings;
throw error;
};
// Used by cjs-loader
export const transformSync = (
code: string,
filePathOrUrl: string,
extendOptions?: TransformOptions,
): Transformed => {
const define: { [key: string]: string } = {};
let url: string;
let filePath: string;
let query: string | undefined;
if (filePathOrUrl.startsWith('file://')) {
url = filePathOrUrl;
const parsed = new URL(filePathOrUrl);
filePath = fileURLToPath(parsed);
} else {
[filePath, query] = filePathOrUrl.split('?');
url = pathToFileURL(filePath) + (query ? `?${query}` : '');
}
if (
!(
filePath.endsWith('.cjs')
|| filePath.endsWith('.cts')
)
) {
define['import.meta.url'] = JSON.stringify(url);
}
const esbuildOptions = {
...cacheConfig,
format: 'cjs',
sourcefile: filePath,
define,
banner: `__filename=${JSON.stringify(filePath)};(()=>{`,
footer: '})()',
// CJS Annotations for Node. Used by ESM loader for CJS interop
platform: 'node',
...extendOptions,
} as const;
const hash = sha1([
code,
JSON.stringify(esbuildOptions),
esbuildVersion,
transformDynamicImportVersion,
].join('-'));
let transformed = cache.get(hash);
if (!transformed) {
transformed = applyTransformersSync(
filePathOrUrl,
code,
[
(_filePath, _code) => {
const patchResult = patchOptions(esbuildOptions);
let result;
try {
result = esbuildTransformSync(_code, esbuildOptions);
} catch (error) {
throw formatEsbuildError(error as TransformFailure);
}
return patchResult(result);
},
(_filePath, _code) => transformDynamicImport(_filePath, _code, true),
],
);
cache.set(hash, transformed);
}
return transformed;
};
// Used by esm-loader
export const transform = async (
code: string,
filePath: string,
extendOptions?: TransformOptions,
): Promise<Transformed> => {
const esbuildOptions = {
...cacheConfig,
format: 'esm',
sourcefile: filePath,
...extendOptions,
} as const;
const hash = sha1([
code,
JSON.stringify(esbuildOptions),
esbuildVersion,
transformDynamicImportVersion,
].join('-'));
let transformed = cache.get(hash);
if (!transformed) {
transformed = await applyTransformers(
filePath,
code,
[
async (_filePath, _code) => {
const patchResult = patchOptions(esbuildOptions);
let result;
try {
result = await esbuildTransform(_code, esbuildOptions);
} catch (error) {
throw formatEsbuildError(error as TransformFailure);
}
return patchResult(result);
},
(_filePath, _code) => transformDynamicImport(_filePath, _code, true),
],
);
cache.set(hash, transformed);
}
return transformed;
};
================================================
FILE: src/utils/transform/transform-dynamic-import.ts
================================================
import MagicString from 'magic-string';
import type { SourceMap } from '@ampproject/remapping';
import { parseEsm } from '../es-module-lexer.js';
export const version = '2';
const toEsmFunctionString = ((imported: Record<string, unknown>) => {
const d = 'default';
if (
imported[d]
&& typeof imported[d] === 'object'
&& '__esModule' in imported[d]
) {
return imported[d];
}
return imported;
}).toString();
const handleDynamicImport = `.then(${toEsmFunctionString})`;
export const transformDynamicImport = (
filePath: string,
code: string,
isMinified?: boolean,
) => {
// Naive check (regex is too slow)
if (isMinified) {
// If minified, we can safely check for "import(" to avoid parsing
if (!code.includes('import(')) {
return;
}
} else if (!code.includes('import')) {
// This is a bit more expensive as we end up parsing even if import statements are detected
return;
}
// Passing in the filePath improves Parsing Error message
const parsed = parseEsm(code, filePath);
const dynamicImports = parsed[0].filter(maybeDynamic => maybeDynamic.d > -1);
if (dynamicImports.length === 0) {
return;
}
const magicString = new MagicString(code);
for (const dynamicImport of dynamicImports) {
magicString.appendRight(dynamicImport.se, handleDynamicImport);
}
const newCode = magicString.toString();
const newMap = magicString.generateMap({
source: filePath,
includeContent: false,
/**
* The performance hit on this is very high
* Since we're only transforming import()s, I think this may be overkill
*/
hires: 'boundary',
}) as unknown as SourceMap;
return {
code: newCode,
map: newMap,
};
};
================================================
FILE: src/utils/tsconfig.ts
================================================
import path from 'node:path';
import {
getTsconfig,
parseTsconfig,
createFilesMatcher,
createPathsMatcher,
type TsConfigResult,
type FileMatcher,
} from 'get-tsconfig';
// eslint-disable-next-line import-x/no-mutable-exports
export let fileMatcher: undefined | FileMatcher;
// eslint-disable-next-line import-x/no-mutable-exports
export let tsconfigPathsMatcher: undefined | ReturnType<typeof createPathsMatcher>;
// eslint-disable-next-line import-x/no-mutable-exports
export let allowJs = false;
export const loadTsconfig = (
configPath?: string,
) => {
let tsconfig: TsConfigResult | null = null;
if (configPath) {
const resolvedConfigPath = path.resolve(configPath);
tsconfig = {
path: resolvedConfigPath,
config: parseTsconfig(resolvedConfigPath),
};
} else {
try {
tsconfig = getTsconfig();
} catch {
// Not warning here for now because it gets warned twice
// Once by ESM loader and then by CJS loader
// const disableWarning = (
// getFlag('--no-warnings', Boolean)
// || Boolean(process.env.NODE_NO_WARNINGS)
// );
// if (!disableWarning) {
// if (error instanceof Error) {
// console.warn(`(tsx:${process.pid}) [-----] TsconfigWarning:`, error.message);
// }
// }
}
if (!tsconfig) {
return;
}
}
fileMatcher = createFilesMatcher(tsconfig);
tsconfigPathsMatcher = createPathsMatcher(tsconfig);
allowJs = tsconfig?.config.compilerOptions?.allowJs ?? false;
};
================================================
FILE: src/utils/url-search-params-stringify.ts
================================================
export const urlSearchParamsStringify = (
searchParams: URLSearchParams,
) => {
// URLSearchParams#size not implemented in Node 18.0.0
const size = Array.from(searchParams).length;
return size > 0 ? `?${searchParams.toString()}` : '';
};
================================================
FILE: src/watch/index.ts
================================================
import type { ChildProcess } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { constants as osConstants } from 'node:os';
import path from 'node:path';
import { command } from 'cleye';
import { watch } from 'chokidar';
import { lightMagenta, lightGreen, yellow } from 'kolorist';
import { run } from '../run.js';
import {
removeArgvFlags,
ignoreAfterArgument,
} from '../remove-argv-flags.js';
import { createIpcServer } from '../utils/ipc/server.js';
import {
clearScreen,
debounce,
log,
} from './utils.js';
const flags = {
noCache: {
type: Boolean,
description: 'Disable caching',
default: false,
},
tsconfig: {
type: String,
description: 'Custom tsconfig.json path',
},
clearScreen: {
type: Boolean,
description: 'Clearing the screen on rerun',
default: true,
},
// Deprecated
ignore: {
type: [String],
description: 'Paths & globs to exclude from being watched (Deprecated: use --exclude)',
},
include: {
type: [String],
description: 'Additional paths & globs to watch',
},
exclude: {
type: [String],
description: 'Paths & globs to exclude from being watched',
},
} as const;
export const watchCommand = command({
name: 'watch',
parameters: ['<script path>'],
flags,
help: {
description: 'Run the script and watch for changes',
},
/**
* ignoreAfterArgument needs to parse the first argument
* because cleye will error on missing arguments
*
* Remove once cleye supports error callbacks on missing arguments
*/
ignoreArgv: ignoreAfterArgument(false),
}, async (argv) => {
const rawArgvs = removeArgvFlags(flags, process.argv.slice(3));
const options = {
noCache: argv.flags.noCache,
tsconfigPath: argv.flags.tsconfig,
clearScreen: argv.flags.clearScreen,
include: argv.flags.include,
exclude: [
...argv.flags.ignore,
...argv.flags.exclude,
],
ipc: true,
};
let runProcess: ChildProcess | undefined;
let exiting = false;
const server = await createIpcServer();
server.on('data', (data) => {
// Collect run-time dependencies to watch
if (
data
&& typeof data === 'object'
&& 'type' in data
&& data.type === 'dependency'
&& 'path' in data
&& typeof data.path === 'string'
) {
const dependencyPath = (
data.path.startsWith('file:')
? fileURLToPath(data.path)
: data.path
);
if (path.isAbsolute(dependencyPath)) {
watcher.add(dependencyPath);
}
}
});
const spawnProcess = () => {
if (exiting) {
return;
}
return run(rawArgvs, options);
};
let waitingChildExit = false;
const killProcess = async (
childProcess: ChildProcess,
signal: NodeJS.Signals = 'SIGTERM',
forceKillOnTimeout = 5000,
) => {
let exited = false;
const waitForExit = new Promise<number | null>((resolve) => {
childProcess.on('exit', (exitCode) => {
exited = true;
waitingChildExit = false;
resolve(exitCode);
});
});
waitingChildExit = true;
childProcess.kill(signal);
setTimeout(() => {
if (!exited) {
log(yellow(`Process didn't exit in ${Math.floor(forceKillOnTimeout / 1000)}s. Force killing...`));
childProcess.kill('SIGKILL');
}
}, forceKillOnTimeout);
return await waitForExit;
};
const reRun = debounce(async (event?: string, filePath?: string) => {
const reason = event ? `${event ? lightMagenta(event) : ''}${filePath ? ` in ${lightGreen(`./${filePath}`)}` : ''}` : '';
if (waitingChildExit) {
log(reason, yellow('Process hasn\'t exited. Killing process...'));
runProcess!.kill('SIGKILL');
return;
}
// If not first run
if (runProcess) {
// If process still running
if (runProcess.exitCode === null) {
log(reason, yellow('Restarting...'));
await killProcess(runProcess);
} else {
log(reason, yellow('Rerunning...'));
}
if (options.clearScreen) {
process.stdout.write(clearScreen);
}
}
runProcess = spawnProcess();
}, 100);
reRun();
const relaySignal = (signal: NodeJS.Signals) => {
// Disable further spawns
exiting = true;
// Child is still running, kill it
if (runProcess?.exitCode === null) {
if (waitingChildExit) {
log(yellow('Previous process hasn\'t exited yet. Force killing...'));
}
killProcess(
runProcess,
// Second Ctrl+C force kills
waitingChildExit ? 'SIGKILL' : signal,
).then(
(exitCode) => {
// eslint-disable-next-line n/no-process-exit
process.exit(exitCode ?? 0);
},
() => {},
);
} else {
// eslint-disable-next-line n/no-process-exit
process.exit(osConstants.signals[signal]);
}
};
process.on('SIGINT', relaySignal);
process.on('SIGTERM', relaySignal);
/**
* Ideally, we can get a list of files loaded from the run above
* and only watch those files, but it's not possible to detect
* the full dependency-tree at run-time because they can be hidden
* in a if-condition/async-delay.
*
* As an alternative, we watch cwd and all run-time dependencies
*/
const watcher = watch(
[
...argv._,
...options.include,
],
{
cwd: process.cwd(),
ignoreInitial: true,
ignored: [
// Hidden directories like .git
'**/.*/**',
// Hidden files (e.g. logs or temp files)
'**/.*',
// 3rd party packages
'**/{node_modules,bower_components,vendor}/**',
...options.exclude,
],
ignorePermissionErrors: true,
},
).on('all', reRun);
// On "Return" key
process.stdin.on('data', () => reRun('Return key'));
});
================================================
FILE: src/watch/utils.ts
================================================
import { gray, lightCyan } from 'kolorist';
const currentTime = () => (new Date()).toLocaleTimeString();
export const log = (...messages: unknown[]) => console.log(
gray(currentTime()),
lightCyan('[tsx]'),
...messages,
);
// From ansi-escapes
// https://github.com/sindresorhus/ansi-escapes/blob/2b3b59c56ff77a/index.js#L80
export const clearScreen = '\u001Bc';
export const debounce = <T extends (this: unknown, ...args: any[]) => void>(
originalFunction: T,
duration: number,
): T => {
let timeout: NodeJS.Timeout | undefined;
return function () {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(
() => Reflect.apply(originalFunction, this, arguments),
duration,
);
} as T;
};
================================================
FILE: tests/fixtures.ts
================================================
import outdent from 'outdent';
import type { PackageJson, TsConfigJson } from 'type-fest';
export const createPackageJson = (packageJson: PackageJson) => JSON.stringify(packageJson);
export const createTsconfig = (tsconfig: TsConfigJson) => JSON.stringify(tsconfig);
const cjsContextCheck = 'typeof module !== \'undefined\'';
const tsCheck = '1 as number';
const declareReact = `
const React = {
createElement: (...args) => Array.from(args),
};
`;
export const jsxCheck = '<><div>JSX</div></>';
const preserveName = `
assert(
(function functionName() {}).name === 'functionName',
'Name should be preserved'
);
`;
const syntaxLowering = `
// es2016 - Exponentiation operator
10 ** 4;
// es2017 - Async functions
(async () => {});
// es2018 - Spread properties
({...Object});
// es2018 - Rest properties
const {...x} = Object;
// es2019 - Optional catch binding
try {} catch {}
// es2020 - Optional chaining
Object?.keys;
// es2020 - Nullish coalescing
Object ?? true
// es2020 - import.meta
// import.meta
// es2021 - Logical assignment operators
// let a = false; a ??= true; a ||= true; a &&= true;
// es2022 - Class instance fields
(class { x });
// es2022 - Static class fields
(class { static x });
// es2022 - Private instance methods
(class { #x() {} });
// es2022 - Private instance fields
(class { #x });
// es2022 - Private static methods
(class { static #x() {} });
// es2022 - Private static fields
(class { static #x });
// es2022 - Class static blocks
(class { static {} });
export const named = 2;
export default 1;
`;
const sourcemap = {
// Adding the dynamic import helps test the import transformation's source map
test: (
extension: string,
) => `import ('node:fs');\nconst { stack } = new Error(); const searchString = 'index.${extension}:SOURCEMAP_LINE'; assert(stack.includes(searchString), \`Expected \${searchString} in stack: \${stack}\`)`,
tag: (
strings: TemplateStringsArray,
...values: string[]
) => {
const finalString = String.raw({ raw: strings }, ...values);
const lineNumber = finalString.split('\n').findIndex(line => line.includes('SOURCEMAP_LINE')) + 1;
return finalString.replaceAll('SOURCEMAP_LINE', lineNumber.toString());
},
};
export const expectErrors = {
'node_modules/expect-errors/index.js': `
exports.expectErrors = async (...assertions) => {
let errors = await Promise.all(
assertions.map(async ([fn, expectedError]) => {
let thrown;
try {
await fn();
} catch (error) {
thrown = error;
}
if (!thrown) {
return new Error('No error thrown');
} else if (
!thrown.message.includes(expectedError)
&& !thrown.stack.includes(expectedError)
) {
return new Error(\`Message \${JSON.stringify(expectedError)} not found in \${JSON.stringify(thrown.message)}\n\${thrown.stack}\`);
}
}),
);
errors = errors.filter(Boolean);
if (errors.length > 0) {
console.error(errors);
process.exitCode = 1;
}
};
`,
};
export const files = {
...expectErrors,
'js/index.js': outdent`
import assert from 'assert';
console.log(JSON.stringify({
importMetaUrl: import.meta.url,
__filename: typeof __filename !== 'undefined' ? __filename : undefined,
}));
${syntaxLowering}
${preserveName}
export const cjsContext = ${cjsContextCheck};
// Implicit directory import works outside of immedaite CWD child
import '../json/'
`,
'json/index.json': JSON.stringify({ 'loaded-file': 'json' }),
'cjs/index.cjs': sourcemap.tag`
const assert = require('node:assert');
assert(${cjsContextCheck}, 'Should have CJS context');
${preserveName}
${sourcemap.test('cjs')}
// Assert __esModule is unwrapped
import ('../ts/index.ts').then((m) => assert(
!(typeof m.default === 'object' && ('default' in m.default)),
));
exports.named = 'named';
// https://github.com/privatenumber/tsx/issues/248
process.setUncaughtExceptionCaptureCallback(console.error);
`,
mjs: {
'index.mjs': outdent`
import assert from 'assert';
import value from './value.mjs';
export const mjsHasCjsContext = ${cjsContextCheck};
assert(value === 1, 'wrong default export');
import ('pkg-commonjs').then((m) => assert(
!(typeof m.default === 'object' && ('default' in m.default)),
));
`,
'value.mjs': 'export default 1',
},
ts: {
'index.ts': sourcemap.tag`
import assert from 'assert';
import type {Type} from 'resolved-by-tsc'
interface Foo {}
type Foo = number
declare module 'foo' {}
enum BasicEnum {
Left,
Right,
}
enum NamedEnum {
SomeEnum = 'some-value',
}
export const a = BasicEnum.Left;
export const b = NamedEnum.SomeEnum;
export default function foo(): string {
return 'foo'
}
// For "ts as tsx" test
const bar = <T>(value: T) => fn<T>();
${preserveName}
${sourcemap.test('ts')}
export const cjsContext = ${cjsContextCheck};
${tsCheck};
`,
'period.in.name.ts': 'export { a } from "."',
dotdot: {
'index.ts': 'export { a } from ".."',
'dotdot/index.ts': 'export { a } from "../.."',
},
'index.js': 'throw new Error("should not be loaded")',
},
// TODO: test resolution priority for files 'index.tsx` & 'index.tsx.ts` via 'index.tsx'
'jsx/index.jsx': sourcemap.tag`
import assert from 'assert';
export const cjsContext = ${cjsContextCheck};
${declareReact}
export const jsx = ${jsxCheck};
${preserveName}
${sourcemap.test('jsx')}
`,
'tsx/index.tsx': sourcemap.tag`
import assert from 'assert';
export const cjsContext = ${cjsContextCheck};
${tsCheck};
${declareReact}
export const jsx = ${jsxCheck};
${preserveName}
${sourcemap.test('tsx')}
`,
'mts/index.mts': sourcemap.tag`
import assert from 'assert';
export const mjsHasCjsContext = ${cjsContextCheck};
${tsCheck};
${preserveName}
${sourcemap.test('mts')}
`,
'cts/index.cts': sourcemap.tag`
const assert = require('assert');
assert(${cjsContextCheck}, 'Should have CJS context');
${tsCheck};
${preserveName}
${sourcemap.test('cts')}
`,
'tsconfig.json': createTsconfig({
compilerOptions: {
paths: {
'@/*': ['./*'],
},
},
}),
'file.txt': 'hello',
'broken-syntax.ts': 'if',
'file-with-sourcemap.js': outdent`
throw new Error;
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiYXNkZi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cblxuXG5cbnRocm93IG5ldyBFcnJvcigpIl0sCiAgIm1hcHBpbmdzIjogIkFBNkJBLE1BQU0sSUFBSSIsCiAgIm5hbWVzIjogW10KfQo=
`,
node_modules: {
'pkg-commonjs': {
'package.json': createPackageJson({
type: 'commonjs',
main: './index.js',
}),
'index.ts': 'throw new Error("should prefer .js over .ts in node_modules")',
'index.js': `
// https://github.com/privatenumber/tsx/issues/726
if (!require.cache) {
throw new Error('require.cache should be defined');
}
${syntaxLowering}
`,
'ts.ts': syntaxLowering,
'cjs.js': `
const _ = exports;
const cjsJs = true;
_.cjsJs = cjsJs;
// Annotate CommonJS exports for Node
0 && (module.exports = {
cjsJs,
});
`,
},
'pkg-module': {
'package.json': createPackageJson({
type: 'module',
main: './index.js',
imports: {
'#*': './*',
},
}),
'index.ts': 'throw new Error("should prefer .js over .ts in node_modules")',
'index.js': `${syntaxLowering}\nexport * from "./empty-export"`,
'empty-export/index.js': 'export {}',
'ts.ts': `${syntaxLowering}\nexport * from "#empty.js"`,
'empty.ts': 'export {}',
},
'pkg-main': {
'package.json': createPackageJson({
main: './index.js',
}),
'index.ts': syntaxLowering,
},
'pkg-exports': {
'package.json': createPackageJson({
exports: {
'.': './index.js',
'./file': './file.js',
'./file.js': './error.js',
'./file.ts': './error.js',
},
}),
'index.ts': syntaxLowering,
'file.js': syntaxLowering,
'error.js': 'throw new Error("should not be loaded")',
},
},
};
================================================
FILE: tests/index.ts
================================================
import { describe } from 'manten';
import { createNode } from './utils/tsx';
import { nodeVersions } from './utils/node-versions';
(async () => {
await describe('tsx', async ({ runTestSuite, describe }) => {
await runTestSuite(import('./specs/repl'));
await runTestSuite(import('./specs/transform'));
for (const nodeVersion of nodeVersions) {
const node = await createNode(nodeVersion);
await describe(`Node ${node.version}`, async ({ runTestSuite }) => {
await runTestSuite(import('./specs/smoke'), node);
await runTestSuite(import('./specs/api'), node);
await runTestSuite(import('./specs/cli'), node);
await runTestSuite(import('./specs/watch'), node);
await runTestSuite(import('./specs/loaders'), node);
await runTestSuite(import('./specs/tsconfig'), node);
});
}
});
})();
================================================
FILE: tests/specs/api.ts
================================================
import path from 'node:path';
import { execaNode } from 'execa';
import { testSuite, expect } from 'manten';
import { createFixture } from 'fs-fixture';
import { outdent } from 'outdent';
import {
tsxCjsPath,
tsxCjsApiPath,
tsxEsmPath,
tsxEsmApiPath,
tsxEsmApiCjsPath,
type NodeApis,
} from '../utils/tsx.js';
import { createPackageJson, createTsconfig, expectErrors } from '../fixtures.js';
const tsFiles = {
'file.ts': outdent`
import { foo } from './foo'
import { json } from './json.json'
export const message = \`\${foo} \${json} \${(typeof __filename === 'undefined' ? import.meta.url : __filename).split(/[\\\\/]/).pop()}\` as string
export { async } from './foo'
`,
'file1.ts': outdent`
import { foo } from './foo?1'
import { json } from './json.json?1'
export const message = \`\${foo} \${json} \${(typeof __filename === 'undefined' ? import.meta.url : __filename).split(/[\\\\/]/).pop()}\` as string
export { async } from './foo?1'
`,
'foo.ts': outdent`
import { setTimeout } from 'node:timers/promises'
import { bar } from './bar.js'
enum Foo {
Foo = 'foo',
}
export const foo = \`\${Foo.Foo} \${bar}\` as string
export const async = setTimeout(10).then(() => require('./async')).catch((error) => error);
`,
cjs: {
node_modules: {
'pkg/index.js': 'module.exports = 1',
},
'exports-no.cts': outdent`
// Supports decorators
const log = (target, key, descriptor) => descriptor;
class Example {
@log
greet() {}
}
console.log("cts loaded" as string)
`,
'exports-yes.cts': 'module.exports = require("./reexport.cjs") as string; require("pkg");',
'esm-syntax.js': 'export const esmSyntax = "esm syntax"',
'reexport.cjs': outdent`
exports.cjsReexport = "cjsReexport";
exports.esmSyntax = require("./esm-syntax.js").esmSyntax;
`,
},
'bar.ts': 'export type A = 1; export { bar } from "pkg"',
'async.ts': 'export default "async"',
'json.json': JSON.stringify({ json: 'json' }),
node_modules: {
pkg: {
'package.json': createPackageJson({
name: 'pkg',
type: 'module',
exports: './pkg.js',
}),
'pkg.js': 'import "node:process"; export const bar = "bar";',
},
'@a/b.cjs': {
'package.json': createPackageJson({
name: '@a/b.cjs',
type: 'module',
exports: './pkg.js',
}),
'pkg.js': 'import "node:process"; export const bar = "bar";',
},
},
'tsconfig.json': createTsconfig({
compilerOptions: {
experimentalDecorators: true,
},
}),
...expectErrors,
};
export default testSuite(({ describe }, node: NodeApis) => {
describe('API', ({ describe }) => {
describe('CommonJS', ({ describe, test }) => {
test('cli', async () => {
await using fixture = await createFixture({
'index.ts': outdent`
import { message } from './file';
console.log(message, new Error().stack);
`,
...tsFiles,
});
const { stdout } = await execaNode(fixture.getPath('index.ts'), {
nodePath: node.path,
nodeOptions: ['--require', tsxCjsPath],
});
expect(stdout).toContain('foo bar');
expect(stdout).toContain('index.ts:3:22');
});
test('loader overwritable from Module', async () => {
await using fixture = await createFixture({
'index.mjs': outdent`
import Module from 'node:module';
const _require = Module.createRequire(import.meta.url);
_require.extensions['.ts'] = () => {};
`,
});
await execaNode(fixture.getPath('index.mjs'), {
nodePath: node.path,
nodeOptions: ['--require', tsxCjsPath],
});
});
test('works with append-transform (nyc)', async () => {
await using fixture = await createFixture({
'index.js': outdent`
import path from 'node:path';
import './ts.ts'
`,
'ts.ts': 'export const ts = "ts" as string',
'hook.js': outdent`
const path = require('path');
const appendTransform = require('append-transform')
appendTransform((code, filename) => {
if (filename.endsWith(path.sep + 'index.js')) {
console.log('js working');
}
return code;
});
appendTransform((code, filename) => {
if (filename.endsWith(path.sep + 'ts.ts')) {
console.log('ts working');
}
return code;
}, '.ts');
`,
'node_modules/append-transform': ({ symlink }) => symlink(path.resolve('node_modules/append-transform'), 'junction'),
});
const { stdout } = await execaNode('./index.js', {
cwd: fixture.path,
nodePath: node.path,
nodeOptions: [
'--require',
'./hook.js',
'--require',
tsxCjsPath,
],
});
expect(stdout).toBe('js working\nts working');
});
describe('register', ({ test }) => {
test('register / unregister', async () => {
await using fixture = await createFixture({
'register.cjs': outdent`
const { register } = require(${JSON.stringify(tsxCjsApiPath)});
try {
require('./file');
} catch {
console.log('Fails as expected');
}
const unregister = register();
const loaded = require('./file');
console.log(loaded.message);
// Remove from cache
const loadedPath = require.resolve('./file');
delete require.cache[loadedPath];
unregister();
try {
require('./file');
} catch {
console.log('Unregistered');
}
`,
...tsFiles,
});
const { stdout } = await execaNode(fixture.getPath('register.cjs'), [],
gitextract_cdughvzf/ ├── .editorconfig ├── .github/ │ ├── DISCUSSION_TEMPLATE/ │ │ └── q-a.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yml │ │ └── config.yml │ ├── renovate.json │ └── workflows/ │ ├── lock-threads.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .nvmrc ├── .vscode/ │ └── settings.json ├── CONTRIBUTING.md ├── FUNDING.json ├── LICENSE ├── README.md ├── docs/ │ ├── .vitepress/ │ │ ├── config.ts │ │ └── theme/ │ │ ├── components/ │ │ │ ├── AsideSponsors.vue │ │ │ ├── ContactForm.vue │ │ │ ├── ImageLink.vue │ │ │ ├── Marquee.vue │ │ │ └── Sponsors.vue │ │ ├── index.ts │ │ └── styles.css │ ├── compilation.md │ ├── contact.md │ ├── dev-api/ │ │ ├── entry-point.md │ │ ├── index.md │ │ ├── node-cli.md │ │ ├── register-cjs.md │ │ ├── register-esm.md │ │ ├── ts-import.md │ │ └── tsx-require.md │ ├── faq.md │ ├── getting-started.md │ ├── index.md │ ├── learn.md │ ├── node-enhancement.md │ ├── package.json │ ├── postcss.config.js │ ├── scripts/ │ │ └── hash-class-names.js │ ├── shell-scripts.md │ ├── tailwind.config.js │ ├── typescript.md │ ├── vscode.md │ └── watch-mode.md ├── package.json ├── pnpm-workspace.yaml ├── release.config.cjs ├── src/ │ ├── @types/ │ │ ├── es-module-lexer.d.ts │ │ └── module.d.ts │ ├── cjs/ │ │ ├── api/ │ │ │ ├── index.ts │ │ │ ├── module-extensions.ts │ │ │ ├── module-resolve-filename/ │ │ │ │ ├── index.ts │ │ │ │ ├── interop-cjs-exports.ts │ │ │ │ ├── is-from-cjs-lexer.ts │ │ │ │ ├── preserve-query.ts │ │ │ │ ├── resolve-implicit-extensions.ts │ │ │ │ └── resolve-ts-extensions.ts │ │ │ ├── register.ts │ │ │ ├── require.ts │ │ │ └── types.ts │ │ └── index.ts │ ├── cli.ts │ ├── esm/ │ │ ├── api/ │ │ │ ├── index.ts │ │ │ ├── register.ts │ │ │ ├── scoped-import.ts │ │ │ └── ts-import.ts │ │ ├── hook/ │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── load.ts │ │ │ ├── package-json.ts │ │ │ ├── resolve.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ └── types.ts │ ├── loader.ts │ ├── patch-repl.ts │ ├── preflight.cts │ ├── remove-argv-flags.ts │ ├── repl.ts │ ├── run.ts │ ├── source-map.ts │ ├── suppress-warnings.cts │ ├── types.ts │ ├── utils/ │ │ ├── debug.ts │ │ ├── es-module-lexer.ts │ │ ├── ipc/ │ │ │ ├── client.ts │ │ │ ├── get-pipe-path.ts │ │ │ └── server.ts │ │ ├── is-windows.ts │ │ ├── map-ts-extensions.ts │ │ ├── node-features.ts │ │ ├── path-utils.ts │ │ ├── read-json-file.ts │ │ ├── sha1.ts │ │ ├── temporary-directory.ts │ │ ├── transform/ │ │ │ ├── apply-transformers.ts │ │ │ ├── cache.ts │ │ │ ├── get-esbuild-options.ts │ │ │ ├── index.ts │ │ │ └── transform-dynamic-import.ts │ │ ├── tsconfig.ts │ │ └── url-search-params-stringify.ts │ └── watch/ │ ├── index.ts │ └── utils.ts ├── tests/ │ ├── fixtures/ │ │ └── test.wasm │ ├── fixtures.ts │ ├── index.ts │ ├── specs/ │ │ ├── api.ts │ │ ├── cli.ts │ │ ├── loaders.ts │ │ ├── repl.ts │ │ ├── smoke.ts │ │ ├── transform.ts │ │ ├── tsconfig.ts │ │ └── watch.ts │ └── utils/ │ ├── coverage-sources-content.ts │ ├── expect-match-in-order.ts │ ├── get-node.ts │ ├── is-process-alive.ts │ ├── is-windows.ts │ ├── node-versions.ts │ ├── package-types.ts │ ├── process-interact.ts │ ├── pty-shell/ │ │ └── index.ts │ └── tsx.ts └── tsconfig.json
SYMBOL INDEX (51 symbols across 21 files)
FILE: docs/scripts/hash-class-names.js
constant TARGET_CLASSES (line 4) | const TARGET_CLASSES = ['VPDocAside'];
constant ALLOWED_EXTENSIONS (line 5) | const ALLOWED_EXTENSIONS = new Set(['.css', '.js', '.vue']);
FILE: src/@types/module.d.ts
type Module (line 5) | interface Module {
type Parent (line 17) | type Parent = {
type LoadFnOutput (line 38) | interface LoadFnOutput {
FILE: src/cjs/api/register.ts
type RegisterOptions (line 35) | type RegisterOptions = {
type Unregister (line 39) | type Unregister = () => void;
type ScopedRequire (line 41) | type ScopedRequire = (
type ScopedResolve (line 46) | type ScopedResolve = (
type NamespacedUnregister (line 52) | type NamespacedUnregister = Unregister & {
type Register (line 58) | type Register = {
FILE: src/cjs/api/types.ts
type LoaderState (line 3) | type LoaderState = {
type ResolveFilename (line 7) | type ResolveFilename = typeof Module._resolveFilename;
type SimpleResolve (line 9) | type SimpleResolve = (request: string) => string;
FILE: src/esm/api/register.ts
type TsconfigOptions (line 8) | type TsconfigOptions = false | string;
type InitializationOptions (line 10) | type InitializationOptions = {
type RegisterOptions (line 16) | type RegisterOptions = {
type Unregister (line 22) | type Unregister = () => Promise<void>;
type NamespacedUnregister (line 24) | type NamespacedUnregister = Unregister & {
type Register (line 29) | type Register = {
FILE: src/esm/api/scoped-import.ts
type ScopedImport (line 5) | type ScopedImport = (
FILE: src/esm/api/ts-import.ts
type Options (line 6) | type Options = {
FILE: src/esm/hook/initialize.ts
type Data (line 6) | type Data = InitializationOptions & {
type GlobalPreloadHook (line 40) | type GlobalPreloadHook = () => string;
FILE: src/esm/hook/resolve.ts
type NextResolve (line 28) | type NextResolve = Parameters<ResolveHook>[2];
FILE: src/esm/types.ts
type Message (line 1) | type Message = {
type TsxRequest (line 8) | type TsxRequest = {
FILE: src/types.ts
type NodeError (line 1) | type NodeError = Error & {
type RequiredProperty (line 7) | type RequiredProperty<Type, Keys extends keyof Type> = Type & { [P in Ke...
FILE: src/utils/ipc/client.ts
type SendToParent (line 4) | type SendToParent = (data: Record<string, unknown>) => void;
type Parent (line 6) | type Parent = {
FILE: src/utils/ipc/server.ts
type OnMessage (line 7) | type OnMessage = (message: Buffer) => void;
FILE: src/utils/node-features.ts
type Version (line 1) | type Version = [number, number, number];
FILE: src/utils/transform/apply-transformers.ts
type MaybePromise (line 3) | type MaybePromise<T> = T | Promise<T>;
type TransformerResult (line 5) | type TransformerResult = {
type Transformer (line 10) | type Transformer<
type Transformed (line 17) | type Transformed = {
FILE: src/utils/transform/cache.ts
class FileCache (line 11) | class FileCache<ReturnType> extends Map<string, ReturnType> {
method constructor (line 33) | constructor() {
method get (line 54) | override get(key: string) {
method set (line 88) | override set(key: string, value: ReturnType) {
method expireDiskCache (line 107) | expireDiskCache() {
method removeOldCacheDirectory (line 118) | async removeOldCacheDirectory() {
FILE: src/utils/transform/get-esbuild-options.ts
constant NODE_DEBUGGER_FLAG_REGEX (line 15) | const NODE_DEBUGGER_FLAG_REGEX = /^--inspect(?:-brk|-port|-publish-uid|-...
FILE: tests/utils/coverage-sources-content.ts
type SourceMapCache (line 4) | type SourceMapCache = Record<string, {
FILE: tests/utils/expect-match-in-order.ts
type Searchable (line 1) | type Searchable = string | RegExp;
FILE: tests/utils/process-interact.ts
type OnTimeoutCallback (line 6) | type OnTimeoutCallback = () => void;
type Api (line 8) | type Api = {
type MaybePromise (line 49) | type MaybePromise<T> = T | Promise<T>;
FILE: tests/utils/tsx.ts
type Options (line 13) | type Options = {
type NodeApis (line 124) | type NodeApis = Awaited<ReturnType<typeof createNode>>;
Condensed preview — 127 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (311K chars).
[
{
"path": ".editorconfig",
"chars": 175,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".github/DISCUSSION_TEMPLATE/q-a.yml",
"chars": 1079,
"preview": "body:\n - type: markdown\n attributes:\n value: |\n ## 💬 What's your question?\n\n <details>\n "
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.yml",
"chars": 3554,
"preview": "name: 🐛 Bug report\ndescription: Found a bug? File a report and let's get it fixed!\nlabels: [bug, pending triage]\nbody:\n "
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 406,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: 💖 [Sponsors only] Feature requests / Help / Questions\n url: http"
},
{
"path": ".github/renovate.json",
"chars": 97,
"preview": "{\n\t\"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n\t\"enabledManagers\": [\"nvm\"]\n}\n"
},
{
"path": ".github/workflows/lock-threads.yml",
"chars": 588,
"preview": "name: Lock threads\n\non:\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * 0\" # Every Sunday\n\njobs:\n lock:\n runs-"
},
{
"path": ".github/workflows/release.yml",
"chars": 1709,
"preview": "name: Release\n\non:\n push:\n branches: [master, develop]\n\npermissions:\n contents: write\n\njobs:\n release:\n name: R"
},
{
"path": ".github/workflows/test.yml",
"chars": 1970,
"preview": "name: Test\non:\n push:\n branches: [master, develop]\n pull_request:\njobs:\n test:\n name: Test\n runs-on: ${{ mat"
},
{
"path": ".gitignore",
"chars": 332,
"preview": "# macOS\n.DS_Store\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Dependency direc"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "20.19.3\n"
},
{
"path": ".vscode/settings.json",
"chars": 54,
"preview": "{\n\t\"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 3924,
"preview": "# Contribution guide\n\nWelcome! We're excited you're interested in contributing. To ensure a smooth and productive collab"
},
{
"path": "FUNDING.json",
"chars": 98,
"preview": "{\n\t\"drips\": {\n\t\t\"ethereum\": {\n\t\t\t\"ownedBy\": \"0xB9C2F7C3BB7734c37d2949B98A90bAea2fd59E21\"\n\t\t}\n\t}\n}\n"
},
{
"path": "LICENSE",
"chars": 1088,
"preview": "MIT License\n\nCopyright (c) Hiroki Osame <hiroki.osame@gmail.com>\n\nPermission is hereby granted, free of charge, to any p"
},
{
"path": "README.md",
"chars": 1373,
"preview": "<h1 align=\"center\">\n<br>\n<picture>\n\t<source media=\"(prefers-color-scheme: dark)\" srcset=\".github/logo-dark.svg\">\n\t<img w"
},
{
"path": "docs/.vitepress/config.ts",
"chars": 4117,
"preview": "import { defineConfig } from 'vitepress';\n\nconst title = 'tsx';\nconst description = 'tsx (TypeScript Execute) - The easi"
},
{
"path": "docs/.vitepress/theme/components/AsideSponsors.vue",
"chars": 761,
"preview": "<template>\n\t<div class=\"mb-8\">\n\t\t<div>\n\t\t\t<h4 class=\"text-base font-semibold\">Premium sponsors</h4>\n\t\t</div>\n\t\t<a\n\t\t\thre"
},
{
"path": "docs/.vitepress/theme/components/ContactForm.vue",
"chars": 2651,
"preview": "<script setup lang=\"ts\">\nimport { ref } from 'vue';\n\nconst isSending = ref(false);\nconst name = ref('');\nconst email = r"
},
{
"path": "docs/.vitepress/theme/components/ImageLink.vue",
"chars": 329,
"preview": "<script setup lang=\"ts\">\ndefineProps<{\n\timgSrc: string;\n\thref: string;\n\talt: string;\n}>();\n</script>\n\n<template>\n\t<a\n\t\tc"
},
{
"path": "docs/.vitepress/theme/components/Marquee.vue",
"chars": 2270,
"preview": "<script setup lang=\"ts\">\nimport { ref, onMounted } from 'vue';\n\nconst reflow = (element: HTMLElement) => {\n\telement.styl"
},
{
"path": "docs/.vitepress/theme/components/Sponsors.vue",
"chars": 711,
"preview": "<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue';\n\nconst svgUrl = 'https://cdn.jsdelivr.net/gh/privatenumbe"
},
{
"path": "docs/.vitepress/theme/index.ts",
"chars": 306,
"preview": "import { h } from 'vue';\nimport DefaultTheme from 'vitepress/theme';\nimport AsideSponsors from './components/AsideSponso"
},
{
"path": "docs/.vitepress/theme/styles.css",
"chars": 354,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n.button {\n\t@apply\n\t\tinline-block\n\t\tpy-2\n\t\tpx-6\n\t\trounded-ful"
},
{
"path": "docs/compilation.md",
"chars": 2362,
"preview": "# Compilation\n\nCompiling your TypeScript files to JavaScript is not handled by _tsx_, but it's a necessary step in most "
},
{
"path": "docs/contact.md",
"chars": 136,
"preview": "# Contact\n\n<script setup lang=\"ts\">\nimport ContactForm from './.vitepress/theme/components/ContactForm.vue';\n</script>\n\n"
},
{
"path": "docs/dev-api/entry-point.md",
"chars": 1100,
"preview": "# Entry-point\n\nImport `tsx` at the top of your entry-file:\n```js\nimport 'tsx'\n\n// Now you can load TS files\nawait import"
},
{
"path": "docs/dev-api/index.md",
"chars": 2043,
"preview": "# Developer API\nThe Developer API allows you to enhance Node.js with _tsx_ without needing to use the `tsx` command.\n\nNo"
},
{
"path": "docs/dev-api/node-cli.md",
"chars": 969,
"preview": "# Node.js CLI\n\nTo use the `node` command directly with _tsx_, pass it as a flag:\n\n```sh\nnode --import tsx ./file.ts\n```\n"
},
{
"path": "docs/dev-api/register-cjs.md",
"chars": 1203,
"preview": "# CommonJS Register API\n\nThe CommonJS Register API allows you to manually register the enhancement at runtime. But note,"
},
{
"path": "docs/dev-api/register-esm.md",
"chars": 1379,
"preview": "# ESM Register API\n\nThe ESM Register API allows you to manually register the enhancement at runtime. But note, this only"
},
{
"path": "docs/dev-api/ts-import.md",
"chars": 1485,
"preview": "# `tsImport()`\n\nNative dynamic `import()` function to import TypeScript files (supports [top-level await](https://v8.dev"
},
{
"path": "docs/dev-api/tsx-require.md",
"chars": 1409,
"preview": "# `tsx.require()`\n\nNative `require()` function enhanced with TypeScript & ESM support.\n\nUse this function for importing "
},
{
"path": "docs/faq.md",
"chars": 10595,
"preview": "<script setup lang=\"ts\">\nimport ImageLink from './.vitepress/theme/components/ImageLink.vue';\n</script>\n\n# Frequently As"
},
{
"path": "docs/getting-started.md",
"chars": 1830,
"preview": "# Getting started\n\n### Prerequisites\n\nBefore you can start using _tsx_, ensure that you have [Node.js installed](https:/"
},
{
"path": "docs/index.md",
"chars": 6144,
"preview": "---\noutline: false\n---\n\n<div class=\"mb-10\">\n<img src=\"/logo-dark.svg\" width=\"150\" class=\"light:hidden\" alt=\"tsx: TypeScr"
},
{
"path": "docs/learn.md",
"chars": 483,
"preview": "# Learning resources\n\nTypeScript can be difficult to get started with and _tsx_ is all about lowering that barrier of en"
},
{
"path": "docs/node-enhancement.md",
"chars": 1743,
"preview": "# Node.js enhancement\n\n## Swap `node` for `tsx`\n\n`tsx` is a drop-in replacement for `node`, meaning you can use it the e"
},
{
"path": "docs/package.json",
"chars": 380,
"preview": "{\n\t\"name\": \"docs\",\n\t\"private\": true,\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"vitepress dev --open\",\n\t\t\"build\": \"vitep"
},
{
"path": "docs/postcss.config.js",
"chars": 75,
"preview": "export default {\n\tplugins: {\n\t\ttailwindcss: {},\n\t\tautoprefixer: {},\n\t},\n};\n"
},
{
"path": "docs/scripts/hash-class-names.js",
"chars": 2010,
"preview": "import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nconst TARGET_CLASSES = ['VPDocAside'];\nconst ALLOWED_E"
},
{
"path": "docs/shell-scripts.md",
"chars": 1020,
"preview": "# Shell scripts\n\nYou can write a shell script in TypeScript by specifying _tsx_ in the [hashbang](https://bash.cyberciti"
},
{
"path": "docs/tailwind.config.js",
"chars": 221,
"preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n\tcontent: [\n\t\t'**/*.md',\n\t\t'.vitepress/**/*.vue',\n\t],\n\tdark"
},
{
"path": "docs/typescript.md",
"chars": 4733,
"preview": "# TypeScript\n\n_tsx_ does not type check your code on its own and expects it to be handled separately. While _tsx_ doesn’"
},
{
"path": "docs/vscode.md",
"chars": 2554,
"preview": "# VS Code debugging\n\nIf you use [VS Code](https://code.visualstudio.com), you can configure it to use _tsx_ to enhance y"
},
{
"path": "docs/watch-mode.md",
"chars": 1491,
"preview": "# Watch mode\n\n::: warning Not to be confused with [Node's Watch mode](https://nodejs.org/docs/latest/api/cli.html#--watc"
},
{
"path": "package.json",
"chars": 2926,
"preview": "{\n\t\"name\": \"tsx\",\n\t\"version\": \"0.0.0-semantic-release\",\n\t\"description\": \"TypeScript Execute (tsx): Node.js enhanced with"
},
{
"path": "pnpm-workspace.yaml",
"chars": 19,
"preview": "packages:\n - docs\n"
},
{
"path": "release.config.cjs",
"chars": 364,
"preview": "const outdent = require('outdent');\n\nmodule.exports = {\n\taddReleases: 'bottom',\n\tsuccessComment: outdent`\n\tThis issue is"
},
{
"path": "src/@types/es-module-lexer.d.ts",
"chars": 82,
"preview": "declare module 'es-module-lexer/js' {\n\texport { parse } from 'es-module-lexer';\n}\n"
},
{
"path": "src/@types/module.d.ts",
"chars": 854,
"preview": "import 'node:module';\n\ndeclare global {\n\tnamespace NodeJS {\n\t\texport interface Module {\n\t\t\t_compile(code: string, filena"
},
{
"path": "src/cjs/api/index.ts",
"chars": 82,
"preview": "export { register } from './register.js';\nexport { require } from './require.js';\n"
},
{
"path": "src/cjs/api/module-extensions.ts",
"chars": 6250,
"preview": "import fs from 'node:fs';\nimport path from 'node:path';\nimport Module from 'node:module';\nimport type { TransformOptions"
},
{
"path": "src/cjs/api/module-resolve-filename/index.ts",
"chars": 2647,
"preview": "import Module from 'node:module';\nimport { fileURLToPath } from 'node:url';\nimport {\n\tisFilePath,\n\tfileUrlPrefix,\n\ttsExt"
},
{
"path": "src/cjs/api/module-resolve-filename/interop-cjs-exports.ts",
"chars": 854,
"preview": "import Module from 'node:module';\n\nexport const getOriginalFilePath = (\n\trequest: string,\n) => {\n\tif (!request.startsWit"
},
{
"path": "src/cjs/api/module-resolve-filename/is-from-cjs-lexer.ts",
"chars": 269,
"preview": "const cjsPreparseCall = 'at cjsPreparseModuleExports (node:internal';\n\nexport const isFromCjsLexer = (\n\terror: Error,\n) "
},
{
"path": "src/cjs/api/module-resolve-filename/preserve-query.ts",
"chars": 2463,
"preview": "import path from 'node:path';\nimport Module from 'node:module';\nimport { urlSearchParamsStringify } from '../../../utils"
},
{
"path": "src/cjs/api/module-resolve-filename/resolve-implicit-extensions.ts",
"chars": 1562,
"preview": "import path from 'node:path';\nimport type { NodeError } from '../../../types.js';\nimport { isDirectoryPattern } from '.."
},
{
"path": "src/cjs/api/module-resolve-filename/resolve-ts-extensions.ts",
"chars": 2629,
"preview": "import { mapTsExtensions } from '../../../utils/map-ts-extensions.js';\nimport type { NodeError } from '../../../types.js"
},
{
"path": "src/cjs/api/register.ts",
"chars": 3802,
"preview": "import Module from 'node:module';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { loadT"
},
{
"path": "src/cjs/api/require.ts",
"chars": 744,
"preview": "import { register, type NamespacedUnregister } from './register.js';\n\nlet api: NamespacedUnregister | undefined;\nconst t"
},
{
"path": "src/cjs/api/types.ts",
"chars": 211,
"preview": "import type Module from 'node:module';\n\nexport type LoaderState = {\n\tenabled: boolean;\n};\n\nexport type ResolveFilename ="
},
{
"path": "src/cjs/index.ts",
"chars": 59,
"preview": "import { register } from './api/register.js';\n\nregister();\n"
},
{
"path": "src/cli.ts",
"chars": 6370,
"preview": "import { constants as osConstants } from 'node:os';\nimport type { ChildProcess, Serializable } from 'node:child_process'"
},
{
"path": "src/esm/api/index.ts",
"chars": 257,
"preview": "export {\n\tregister,\n\ttype InitializationOptions,\n\ttype NamespacedUnregister,\n\ttype Register,\n\ttype RegisterOptions,\n\ttyp"
},
{
"path": "src/esm/api/register.ts",
"chars": 2852,
"preview": "import module from 'node:module';\nimport { MessageChannel, type MessagePort } from 'node:worker_threads';\nimport type { "
},
{
"path": "src/esm/api/scoped-import.ts",
"chars": 766,
"preview": "import { pathToFileURL } from 'node:url';\nimport type { TsxRequest } from '../types.js';\nimport { fileUrlPrefix } from '"
},
{
"path": "src/esm/api/ts-import.ts",
"chars": 2341,
"preview": "import { register as cjsRegister } from '../../cjs/api/index.js';\nimport { isFeatureSupported, esmLoadReadFile } from '."
},
{
"path": "src/esm/hook/index.ts",
"chars": 135,
"preview": "export { initialize, globalPreload } from './initialize.js';\nexport { load } from './load.js';\nexport { resolve } from '"
},
{
"path": "src/esm/hook/initialize.ts",
"chars": 1234,
"preview": "import type { InitializeHook } from 'node:module';\nimport type { InitializationOptions } from '../api/register.js';\nimpo"
},
{
"path": "src/esm/hook/load.ts",
"chars": 4944,
"preview": "import { fileURLToPath } from 'node:url';\nimport path from 'node:path';\nimport type { LoadHook } from 'node:module';\nimp"
},
{
"path": "src/esm/hook/package-json.ts",
"chars": 1829,
"preview": "import fs from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { PackageJson } from 'type-fest';\n\nconst"
},
{
"path": "src/esm/hook/resolve.ts",
"chars": 8843,
"preview": "import path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type {\n\tResolveHook,\n\tResolveHookContext,"
},
{
"path": "src/esm/hook/utils.ts",
"chars": 1017,
"preview": "import path from 'node:path';\nimport { tsExtensions } from '../../utils/path-utils.js';\nimport { getPackageType } from '"
},
{
"path": "src/esm/index.ts",
"chars": 315,
"preview": "import { isMainThread } from 'node:worker_threads';\nimport { isFeatureSupported, moduleRegister } from '../utils/node-fe"
},
{
"path": "src/esm/types.ts",
"chars": 175,
"preview": "export type Message = {\n\ttype: 'deactivated';\n} | {\n\ttype: 'load';\n\turl: string;\n};\n\nexport type TsxRequest = {\n\tnamespa"
},
{
"path": "src/loader.ts",
"chars": 290,
"preview": "// Hook require() to transform to CJS\n// eslint-disable-next-line import-x/no-unresolved, @typescript-eslint/no-require-"
},
{
"path": "src/patch-repl.ts",
"chars": 896,
"preview": "import repl, { type REPLServer, type REPLEval } from 'node:repl';\nimport { transform } from 'esbuild';\n\nconst patchEval "
},
{
"path": "src/preflight.cts",
"chars": 2465,
"preview": "import { constants as osConstants } from 'node:os';\nimport { isMainThread } from 'node:worker_threads';\nimport { connect"
},
{
"path": "src/remove-argv-flags.ts",
"chars": 586,
"preview": "import {\n\ttypeFlag,\n\ttype Flags,\n\ttype TypeFlagOptions,\n} from 'type-flag';\n\nexport const ignoreAfterArgument = (\n\tignor"
},
{
"path": "src/repl.ts",
"chars": 1064,
"preview": "// Deprecated: Delete entry-point in next major in favor of patch-repl.ts\n\nimport repl, { type REPLEval } from 'node:rep"
},
{
"path": "src/run.ts",
"chars": 1287,
"preview": "import type { StdioOptions } from 'node:child_process';\nimport { pathToFileURL } from 'node:url';\nimport spawn from 'cro"
},
{
"path": "src/source-map.ts",
"chars": 500,
"preview": "import type { Transformed } from './utils/transform/apply-transformers.js';\n\nconst inlineSourceMapPrefix = '\\n//# source"
},
{
"path": "src/suppress-warnings.cts",
"chars": 880,
"preview": "// Deprecated: Move to preflight.cts & delete entry-point in next major\n\nconst ignoreWarnings = new Set([\n\n\t// v18.0.0\n\t"
},
{
"path": "src/types.ts",
"chars": 181,
"preview": "export type NodeError = Error & {\n\tcode: string;\n\turl?: string;\n\tpath?: string;\n};\n\nexport type RequiredProperty<Type, K"
},
{
"path": "src/utils/debug.ts",
"chars": 1570,
"preview": "import { inspect } from 'node:util';\nimport { writeSync } from 'node:fs';\nimport {\n\toptions, bgBlue, black, bgLightYello"
},
{
"path": "src/utils/es-module-lexer.ts",
"chars": 1170,
"preview": "import { parse as parseJs } from 'es-module-lexer/js';\n\nlet parseWasm: typeof import('es-module-lexer').parse | undefine"
},
{
"path": "src/utils/ipc/client.ts",
"chars": 1184,
"preview": "import net from 'node:net';\nimport { getPipePath } from './get-pipe-path.js';\n\nexport type SendToParent = (data: Record<"
},
{
"path": "src/utils/ipc/get-pipe-path.ts",
"chars": 314,
"preview": "import path from 'node:path';\nimport { tmpdir } from '../temporary-directory.js';\nimport { isWindows } from '../is-windo"
},
{
"path": "src/utils/ipc/server.ts",
"chars": 2394,
"preview": "import net from 'node:net';\nimport fs from 'node:fs';\nimport { tmpdir } from '../temporary-directory.js';\nimport { isWin"
},
{
"path": "src/utils/is-windows.ts",
"chars": 55,
"preview": "export const isWindows = process.platform === 'win32';\n"
},
{
"path": "src/utils/map-ts-extensions.ts",
"chars": 1786,
"preview": "import path from 'node:path';\nimport { isFilePath, fileUrlPrefix, nodeModulesPath } from './path-utils.js';\n\nconst impli"
},
{
"path": "src/utils/node-features.ts",
"chars": 1576,
"preview": "export type Version = [number, number, number];\n\n// Is v1 greater or equal to v2?\nconst isVersionGreaterOrEqual = (v1: V"
},
{
"path": "src/utils/path-utils.ts",
"chars": 1727,
"preview": "import path from 'node:path';\n\n/**\n * Prior to calling this function, it's expected that Windows paths have been filtere"
},
{
"path": "src/utils/read-json-file.ts",
"chars": 222,
"preview": "import fs from 'node:fs';\n\nexport const readJsonFile = <JsonType>(\n\tfilePath: string | URL,\n) => {\n\ttry {\n\t\tconst jsonSt"
},
{
"path": "src/utils/sha1.ts",
"chars": 141,
"preview": "import crypto from 'node:crypto';\n\nexport const sha1 = (data: string) => (\n\tcrypto\n\t\t.createHash('sha1')\n\t\t.update(data)"
},
{
"path": "src/utils/temporary-directory.ts",
"chars": 569,
"preview": "import path from 'node:path';\nimport os from 'node:os';\n\n/**\n * Cache directory is based on the user's identifier\n * to "
},
{
"path": "src/utils/transform/apply-transformers.ts",
"chars": 1408,
"preview": "import remapping, { type SourceMap, type SourceMapInput } from '@ampproject/remapping';\n\ntype MaybePromise<T> = T | Prom"
},
{
"path": "src/utils/transform/cache.ts",
"chars": 3279,
"preview": "import fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport { readJsonFile } from '../read-"
},
{
"path": "src/utils/transform/get-esbuild-options.ts",
"chars": 2641,
"preview": "import path from 'node:path';\nimport type { TransformOptions, TransformResult } from 'esbuild';\nimport type { SourceMap "
},
{
"path": "src/utils/transform/index.ts",
"chars": 3507,
"preview": "import { pathToFileURL, fileURLToPath } from 'node:url';\nimport {\n\ttransform as esbuildTransform,\n\ttransformSync as esbu"
},
{
"path": "src/utils/transform/transform-dynamic-import.ts",
"chars": 1665,
"preview": "import MagicString from 'magic-string';\nimport type { SourceMap } from '@ampproject/remapping';\nimport { parseEsm } from"
},
{
"path": "src/utils/tsconfig.ts",
"chars": 1454,
"preview": "import path from 'node:path';\nimport {\n\tgetTsconfig,\n\tparseTsconfig,\n\tcreateFilesMatcher,\n\tcreatePathsMatcher,\n\ttype TsC"
},
{
"path": "src/utils/url-search-params-stringify.ts",
"chars": 242,
"preview": "export const urlSearchParamsStringify = (\n\tsearchParams: URLSearchParams,\n) => {\n\t// URLSearchParams#size not implemente"
},
{
"path": "src/watch/index.ts",
"chars": 5463,
"preview": "import type { ChildProcess } from 'node:child_process';\nimport { fileURLToPath } from 'node:url';\nimport { constants as "
},
{
"path": "src/watch/utils.ts",
"chars": 723,
"preview": "import { gray, lightCyan } from 'kolorist';\n\nconst currentTime = () => (new Date()).toLocaleTimeString();\n\nexport const "
},
{
"path": "tests/fixtures.ts",
"chars": 7996,
"preview": "import outdent from 'outdent';\nimport type { PackageJson, TsConfigJson } from 'type-fest';\n\nexport const createPackageJs"
},
{
"path": "tests/index.ts",
"chars": 824,
"preview": "import { describe } from 'manten';\nimport { createNode } from './utils/tsx';\nimport { nodeVersions } from './utils/node-"
},
{
"path": "tests/specs/api.ts",
"chars": 26931,
"preview": "import path from 'node:path';\nimport { execaNode } from 'execa';\nimport { testSuite, expect } from 'manten';\nimport { cr"
},
{
"path": "tests/specs/cli.ts",
"chars": 11918,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport { testSuite, expect } from 'manten';\nimport { createFixture } "
},
{
"path": "tests/specs/loaders.ts",
"chars": 2735,
"preview": "import { testSuite, expect } from 'manten';\nimport { createFixture } from 'fs-fixture';\nimport type { NodeApis } from '."
},
{
"path": "tests/specs/repl.ts",
"chars": 2305,
"preview": "import { testSuite } from 'manten';\nimport { tsx } from '../utils/tsx';\nimport { processInteract } from '../utils/proces"
},
{
"path": "tests/specs/smoke.ts",
"chars": 18571,
"preview": "import path from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { testSuite, expect } from 'manten';\nimpo"
},
{
"path": "tests/specs/transform.ts",
"chars": 3937,
"preview": "import { testSuite, expect } from 'manten';\nimport { createFsRequire } from 'fs-require';\nimport { Volume } from 'memfs'"
},
{
"path": "tests/specs/tsconfig.ts",
"chars": 5536,
"preview": "import { testSuite, expect } from 'manten';\nimport { createFixture } from 'fs-fixture';\nimport type { NodeApis } from '."
},
{
"path": "tests/specs/watch.ts",
"chars": 8492,
"preview": "import { setTimeout } from 'node:timers/promises';\nimport { testSuite, expect } from 'manten';\nimport { createFixture } "
},
{
"path": "tests/utils/coverage-sources-content.ts",
"chars": 694,
"preview": "import path from 'node:path';\nimport fs from 'node:fs/promises';\n\ntype SourceMapCache = Record<string, {\n\tdata: {\n\t\tsour"
},
{
"path": "tests/utils/expect-match-in-order.ts",
"chars": 1093,
"preview": "type Searchable = string | RegExp;\n\nconst stringify = (\n\tvalue: { toString(): string },\n) => JSON.stringify(\n\tvalue.toSt"
},
{
"path": "tests/utils/get-node.ts",
"chars": 438,
"preview": "import _getNode from 'get-node';\n\nexport const getNode = async (\n\tnodeVersion: string,\n) => {\n\tif (nodeVersion === proce"
},
{
"path": "tests/utils/is-process-alive.ts",
"chars": 125,
"preview": "export const isProcessAlive = (pid: number) => {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch {}\n\treturn false"
},
{
"path": "tests/utils/is-windows.ts",
"chars": 55,
"preview": "export const isWindows = process.platform === 'win32';\n"
},
{
"path": "tests/utils/node-versions.ts",
"chars": 534,
"preview": "/**\n * The specific version is used in CI because of caching\n * Only pull the latest major version in development\n * and"
},
{
"path": "tests/utils/package-types.ts",
"chars": 78,
"preview": "export const packageTypes = [\n\tundefined,\n\t'module',\n\t'commonjs',\n] as const;\n"
},
{
"path": "tests/utils/process-interact.ts",
"chars": 1880,
"preview": "import type { Readable } from 'node:stream';\nimport { on } from 'node:events';\nimport { setTimeout } from 'node:timers/p"
},
{
"path": "tests/utils/pty-shell/index.ts",
"chars": 881,
"preview": "import { spawn, waitFor } from 'pty-spawn';\nimport stripAnsi from 'strip-ansi';\n\nexport const isWindows = process.platfo"
},
{
"path": "tests/utils/tsx.ts",
"chars": 3089,
"preview": "import { fileURLToPath } from 'node:url';\nimport { execaNode, type NodeOptions } from 'execa';\nimport {\n\tisFeatureSuppor"
},
{
"path": "tsconfig.json",
"chars": 343,
"preview": "{\n\t\"compilerOptions\": {\n\t\t\"target\": \"es2022\",\n\t\t\"moduleDetection\": \"force\",\n\n\n\t\t\"module\": \"preserve\",\n\t\t\"resolveJsonModu"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the privatenumber/tsx GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 127 files (263.6 KB), approximately 76.4k tokens, and a symbol index with 51 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.