Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
04f2797
feat(migrate): migrate tsx scripts to vpx
fengmk2 Oct 21, 2026
2d74237
fix(migrate): keep tsx where vpx would fail or lose it
fengmk2 Oct 21, 2026
c7407e0
fix(migrate): follow cd and find more tsx uses before removing it
fengmk2 Oct 21, 2026
e3d65ff
fix(migrate): rewrite quoted tsx only for launchers, check more Commo…
fengmk2 Oct 21, 2026
0522e9a
fix(migrate): find tsx in tool settings, keep require('./a.js') entries
fengmk2 Oct 21, 2026
5d51c69
fix(migrate): rewrite only command arguments of launchers, keep tsx-o…
fengmk2 Oct 21, 2026
5946649
fix(migrate): keep tsx for watched tests and launched TSX_TSCONFIG_PATH
fengmk2 Oct 21, 2026
f799a45
fix(migrate): detect compact ES module syntax, follow TSX_TSCONFIG_PA…
fengmk2 Oct 21, 2026
8c8fcc4
fix(migrate): handle cd fallbacks, %VAR%, and Windows tsx bin paths
fengmk2 Oct 21, 2026
823675b
style(migrate): use Array#at for the last token
fengmk2 Oct 21, 2026
e95bb39
fix(migrate): detect shebang-only tsx projects, scan JSONC configs
fengmk2 Oct 21, 2026
fc3977f
fix(migrate): refine the tsx script checks and use scan
fengmk2 Oct 21, 2026
69138ca
fix(migrate): keep tsx for large globs, scan TOML and argv-array laun…
fengmk2 Oct 21, 2026
139f207
fix(migrate): keep tsx for unreadable entries and unresolvable requires
fengmk2 Oct 21, 2026
a7999c7
fix(migrate): count more module uses, skip concurrently option values
fengmk2 Oct 21, 2026
ecd57a6
fix(migrate): drop redundant tsx loader flags, find more loader refer…
fengmk2 Oct 21, 2026
3785723
fix(migrate): keep tsx for decorators vpx cannot compile and unmatche…
fengmk2 Oct 21, 2026
9a3451f
fix(migrate): check preloads, env files, and --tsconfig before rewrit…
fengmk2 Oct 21, 2026
0daf722
refactor(migrate): simplify the tsx checks, fix false retention
fengmk2 Oct 22, 2026
e6d9b40
fix(migrate): find annotated tsx imports, bare imports aliases, npx -…
fengmk2 Oct 22, 2026
464cc7f
fix(migrate): check tsx preloads given as absolute paths
fengmk2 Oct 22, 2026
f7b188d
test(migrate): keep the absolute preload test free of Windows temp paths
fengmk2 Oct 22, 2026
ecac02a
fix(migrate): keep tsx for bare lint-staged tasks and loader settings
fengmk2 Oct 22, 2026
227cc4c
Merge branch 'main' into feat/migrate-tsx-to-vpx
fengmk2 Oct 22, 2026
6c3f3f8
Merge branch 'main' into feat/migrate-tsx-to-vpx
fengmk2 Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "tsx",
"version": "4.20.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "migration-tsx",
"type": "module",
"scripts": {
"dev": "tsx watch --clear-screen=false src/server.ts",
"seed": "cross-env NODE_ENV=development npx tsx scripts/seed.ts",
"bench": "node --import tsx ./scripts/bench.ts",
"start": "concurrently \"tsx watch src/server.ts\" \"vp dev\""
},
"devDependencies": {
"tsx": "file:.fixture/tsx",
"vite": "^7.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const runs: number = 100;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env tsx
const version: string = '1.0.0';
console.log(version);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const rows: number = 10;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[[case]]
name = "migration_tsx_scripts"
vp = "local"
comment = "tsx comes from a local stub package so the install stays offline"
steps = [
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "tsx scripts should migrate to vpx automatically", continue-on-failure = true },
{ argv = ["vpt", "print-file", "package.json"], comment = "scripts run vpx, and the tsx devDependency is gone", continue-on-failure = true },
{ argv = ["vpt", "print-file", "scripts/release.ts"], comment = "the shebang runs vpx", continue-on-failure = true },
]

[[case]]
name = "migration_tsx_kept"
vp = "local"
comment = "tsx comes from a local stub package so the install stays offline"
steps = [
{ argv = ["vpt", "json-edit", "package.json", "scripts.repl", "tsx"], snapshot = false },
{ argv = ["vpt", "json-edit", "package.json", "scripts.calc", "tsx -e 'console.log(1 + 1)'"], snapshot = false },
{ argv = ["vp", "migrate", "--no-interactive", "--no-hooks", "--no-agent", "--no-editor"], comment = "scripts vpx cannot run keep tsx, with a warning for each", continue-on-failure = true },
{ argv = ["vpt", "print-file", "package.json"], comment = "the other scripts run vpx, and the tsx devDependency stays", continue-on-failure = true },
]

[[case]]
name = "migration_tsx_already_vite_plus"
vp = "local"
comment = "On a project already using Vite+, the tsx migration belongs to `vp migrate --full`."
steps = [
{ argv = ["vpt", "json-edit", "package.json", "devDependencies.vite-plus", "latest"], snapshot = false },
{ argv = ["vp", "migrate", "--no-interactive"], comment = "a plain upgrade leaves tsx scripts to --full", continue-on-failure = true },
{ argv = ["vp", "migrate", "--no-interactive", "--full", "--no-hooks", "--no-agent", "--no-editor"], comment = "--full migrates tsx scripts to vpx", continue-on-failure = true },
{ argv = ["vpt", "print-file", "package.json"], comment = "scripts run vpx, and the tsx devDependency is gone", continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# migration_tsx_already_vite_plus

On a project already using Vite+, the tsx migration belongs to `vp migrate --full`.

## `vpt json-edit package.json devDependencies.vite-plus latest`


## `vp migrate --no-interactive`

a plain upgrade leaves tsx scripts to --full

```
VITE+ - The Unified Toolchain for the Web

◇ Updated . to Vite+ <version>
• Node <version> pnpm <version>
• Dependencies:
vite-plus latest → <version>
vite → <version>
• Package manager settings configured
• Skipped editor, hooks, and lint setup. Run `vp migrate --full` to apply them.
```

## `vp migrate --no-interactive --full --no-hooks --no-agent --no-editor`

--full migrates tsx scripts to vpx

```
VITE+ - The Unified Toolchain for the Web

▲ No package manager is declared in package.json; using pnpm for this migration without adding a pin. Run `vp env pin` to declare it explicitly.
● tsx scripts detected. Auto-migrating to vpx...
◇ Updated . to Vite+ <version>
• Node <version> pnpm <version>
• Dependencies:
vite → <version>
• tsx scripts migrated to `vpx`
```

## `vpt print-file package.json`

scripts run vpx, and the tsx devDependency is gone

```
{
"devDependencies": {
"vite": "catalog:",
"vite-plus": "catalog:"
},
"name": "migration-tsx",
"scripts": {
"bench": "vpx ./scripts/bench.ts",
"dev": "vpx --watch --clear-screen=false src/server.ts",
"seed": "cross-env NODE_ENV=development vpx scripts/seed.ts",
"start": "concurrently \"vpx --watch src/server.ts\" \"vp dev\""
},
"type": "module"
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# migration_tsx_kept

tsx comes from a local stub package so the install stays offline

## `vpt json-edit package.json scripts.repl tsx`


## `vpt json-edit package.json scripts.calc 'tsx -e '\''console.log(1 + 1)'\'''`


## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`

scripts vpx cannot run keep tsx, with a warning for each

```
VITE+ - The Unified Toolchain for the Web

● tsx scripts detected. Auto-migrating to vpx...
◇ Migrated . to Vite+ <version>
• Node <version> pnpm <version>
• 1 config update applied
• tsx scripts migrated to `vpx`
! Warnings:
- Kept tsx in package.json scripts "calc": `vpx` does not run TypeScript passed to --eval or --print.
- Kept tsx in package.json scripts "repl": `vpx` does not start a TypeScript REPL.
- Kept the tsx devDependency in package.json: `scripts` in package.json still uses tsx.
```

## `vpt print-file package.json`

the other scripts run vpx, and the tsx devDependency stays

```
{
"devDependencies": {
"tsx": "file:.fixture/tsx",
"vite": "catalog:",
"vite-plus": "catalog:"
},
"name": "migration-tsx",
"scripts": {
"bench": "vpx ./scripts/bench.ts",
"calc": "tsx -e 'console.log(1 + 1)'",
"dev": "vpx --watch --clear-screen=false src/server.ts",
"repl": "tsx",
"seed": "cross-env NODE_ENV=development vpx scripts/seed.ts",
"start": "concurrently \"vpx --watch src/server.ts\" \"vp dev\""
},
"type": "module"
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# migration_tsx_scripts

tsx comes from a local stub package so the install stays offline

## `vp migrate --no-interactive --no-hooks --no-agent --no-editor`

tsx scripts should migrate to vpx automatically

```
VITE+ - The Unified Toolchain for the Web

● tsx scripts detected. Auto-migrating to vpx...
◇ Migrated . to Vite+ <version>
• Node <version> pnpm <version>
• 1 config update applied
• tsx scripts migrated to `vpx`
```

## `vpt print-file package.json`

scripts run vpx, and the tsx devDependency is gone

```
{
"name": "migration-tsx",
"type": "module",
"scripts": {
"dev": "vpx --watch --clear-screen=false src/server.ts",
"seed": "cross-env NODE_ENV=development vpx scripts/seed.ts",
"bench": "vpx ./scripts/bench.ts",
"start": "concurrently \"vpx --watch src/server.ts\" \"vp dev\""
},
"devDependencies": {
"vite": "catalog:",
"vite-plus": "catalog:"
}
}
```

## `vpt print-file scripts/release.ts`

the shebang runs vpx

```
#!/usr/bin/env vpx
const version: string = '1.0.0';
console.log(version);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const port: number = 3000;
26 changes: 24 additions & 2 deletions docs/guide/migrate-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Migration is idempotent: running it again after a successful migration should no

On a project that already depends on `vite-plus`, `vp migrate` performs an upgrade only: it updates dependencies and package-manager configuration and finalizes imports. It does not touch project setup.

- `--full` also runs the setup actions: git hooks, editor config, agent files, ESLint, Prettier, and tsup migration, framework shims, the tsconfig `baseUrl` fix, and the Volta to `.node-version` conversion.
- `--full` also runs the setup actions: git hooks, editor config, agent files, ESLint, Prettier, tsup, and tsx migration, framework shims, the tsconfig `baseUrl` fix, and the Volta to `.node-version` conversion.
- `--hooks`, `--agent`, and `--editor` opt into a single setup action without `--full`.

When a default upgrade skips setup actions that would apply, it prints a hint to run `vp migrate --full`. Fresh (non Vite+) projects always run the full migration.
Expand Down Expand Up @@ -261,8 +261,9 @@ Migration rewrites commands provided by the Vite+ toolchain in `package.json` sc
| `eslint` | `vp lint`, when its optional migration runs |
| `prettier` | `vp fmt`, when its optional migration runs |
| `tsup` | `vp pack`, when its optional migration runs |
| `tsx` | `vpx`, when its optional migration runs |
Comment thread
fengmk2 marked this conversation as resolved.
Resolved

For commands launched through `bunx`, migration preserves `bunx` and its `--bun` flag (keeping the user's chosen runtime) and rewrites only the managed command. This also works when `bunx` follows a command-launcher delimiter such as `run` or `--`:
For commands launched through `bunx`, migration preserves `bunx` and its `--bun` flag (keeping the user's chosen runtime) and rewrites only the managed command. tsx is the exception: `bunx tsx` becomes `vpx`, and `bunx --bun tsx` keeps tsx (see [tsx to `vpx`](#tsx-to-vpx)). This also works when `bunx` follows a command-launcher delimiter such as `run` or `--`:

| Before | After |
| ------------------------------------------------------- | -------------------------------------------------------- |
Expand All @@ -273,6 +274,27 @@ For commands launched through `bunx`, migration preserves `bunx` and its `--bun`

Unrelated `bunx` commands and other package-executor forms remain unchanged.

### tsx to `vpx`

When a `package.json` script, a lint-staged task, or a `#!/usr/bin/env tsx` shebang runs [tsx](https://tsx.is), migration offers to move it to [`vpx`](/guide/vpx), Vite+'s TypeScript script runner. It rewrites scripts and lint-staged tasks in `package.json` and JSON lint-staged files, and `#!/usr/bin/env tsx` shebangs in script files:

| Before | After |
| -------------------------------------------------------------------------- | ------------------------------------------ |
| `tsx src/seed.ts --rows 10` | `vpx src/seed.ts --rows 10` |
| `tsx watch --include "*.json" src/a.ts` | `vpx --watch --include "*.json" src/a.ts` |
| `npx tsx a.ts`, `pnpm exec tsx a.ts`, `pnpm dlx tsx a.ts`, `bunx tsx a.ts` | `vpx a.ts` |
| `node --import tsx ./a.ts` | `vpx ./a.ts` |
| `cross-env NODE_ENV=dev tsx a.ts` | `cross-env NODE_ENV=dev vpx a.ts` |
| `concurrently "tsx watch a.ts" "vp dev"` | `concurrently "vpx --watch a.ts" "vp dev"` |

`tsx watch` becomes `vpx --watch`, never `vpx watch`, which runs the npm package `watch`. Node.js options and tsx watch options keep their order, and `--no-cache` is dropped. A quoted command is rewritten only for programs that run it, such as `concurrently`, `nodemon --exec`, `sh -c`, and `cross-env-shell`; a quoted argument of other programs, as in `echo "tsx a.ts"`, is data. `vpx` needs the same Node.js versions as Vite+ itself, and migration already upgrades incompatible runtime pins to them (see [Vitest 5](/guide/vitest-v5)).

Migration keeps tsx, with a warning, for a command `vpx` cannot run: `--eval`, `--print`, or `--check`, the REPL, `--test` without files or in watch mode, an entry without a file extension, commands that set `TSX_TSCONFIG_PATH` (pass `--tsconfig` to `vpx` instead) or load tsx through `NODE_OPTIONS`, and commands with pipes, variables, or subshells. It also reads the scripts a command runs, including local `--require` and `--import` modules, and keeps tsx when one does not exist yet, as when an earlier command generates it, and when `vpx` would fail on one: a `require()` of `./a.js` that only tsx resolves to `a.ts`, and, because tsx compiles ES module syntax to CommonJS, a `.cts` file with `import` or `export`, and a file that mixes `import` with `__dirname`, `__filename`, `require()`, or `module.exports` in a package without `"type": "module"`. This check skips globs and scripts after a `cd` or another shell state change, and reads only the files a command names, not the modules they import. It does not look for what else `vpx` handles differently, such as standard decorators, tsconfig `paths` in `require()`, or a tsconfig `module` of `node16` or `nodenext`; `vpx` reports those when the script runs. A package that lists tsx in `dependencies`, `optionalDependencies`, or `peerDependencies` keeps it too, since `vpx` comes from the `vite-plus` devDependency.

Migration removes the `tsx` devDependency only when nothing uses tsx anymore: no kept script, no import of `tsx` or its subpaths such as `tsx/esm/api` (in source, build output, or a `package.json` `imports` alias), no shebang, no tool setting such as `--import=tsx` in `package.json` or a JSON or JavaScript config, and no tsx command in CI workflows, git hooks, or shell scripts. The root's dependency stays while any workspace package still uses tsx, and a package's dependency stays while a nested package under it does. When tsx stays, the warning names what still uses it.

See [Coming from tsx](/guide/vpx#coming-from-tsx) for the other differences.

## Continuous Integration Rules

Migration replaces exact `voidzero-dev/setup-vp@v1` references in GitHub Actions workflows and composite actions under `.github` with the latest exact `setup-vp` release known to that Vite+ version. The frozen `v1` tag does not receive new releases. Existing exact versions and commit SHAs are left unchanged.
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { migrationPrompt, upgradePrompt } from '../.vitepress/theme/data/migrati

## Overview

This command is the starting point for consolidating separate Vite, Vitest, Oxlint, Oxfmt, ESLint, Prettier, tsdown, and tsup setups into Vite+.
This command is the starting point for consolidating separate Vite, Vitest, Oxlint, Oxfmt, ESLint, Prettier, tsdown, tsup, and tsx setups into Vite+.

Use it when you want to take an existing project and move it onto the Vite+ defaults instead of wiring each tool by hand.

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/vpx.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ JSX that depends on a compiler plugin is not supported. `vpx` compiles JSX only

### Coming from tsx

Most scripts that run with tsx run with `vpx` unchanged. These differences can need a change:
`vp migrate` rewrites tsx commands in `package.json` scripts to `vpx` and removes the `tsx` devDependency once nothing uses it; see [tsx to `vpx`](/guide/migrate-rules#tsx-to-vpx). Most scripts that run with tsx run with `vpx` unchanged. These differences can need a change:

- **JSX default**: without a tsconfig `jsx` option, tsx compiles JSX to `React.createElement()`, which needs `React` in scope, while `vpx` uses the automatic runtime. Set `"jsx": "react"` to keep the classic output. Because of its default, tsx also ignores a lone `/** @jsxImportSource … */` comment unless the file has `/** @jsxRuntime automatic */` or the tsconfig sets `"jsx": "react-jsx"`; `vpx` applies the comment on its own.
- **`.cts` files with `import` or `export`**: tsx compiles them to CommonJS, and `vpx` reports an error. Rename the file to `.mts`, or write it with `import x = require()` and `export =`.
Expand Down
Loading
Loading