Skip to content
Draft
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
bcfdb84
perf(lint): benchmark the experimental tsrs-lint backend
fengmk2 Nov 4, 2026
6355fd9
perf(lint): use tsrs as the experimental default backend
fengmk2 Nov 5, 2026
947811c
chore(deps): update tsrs to 0.22.0-ts7.1.0-beta
fengmk2 Nov 5, 2026
2a152a9
docs(rfc): describe the default tsrs backend
fengmk2 Nov 5, 2026
aaa2a6f
bench(lint): add reproducible Codespaces resource comparison
fengmk2 Nov 5, 2026
94c63a8
bench(lint): report tsrs 0.22 time and memory results
fengmk2 Nov 5, 2026
9df490e
test(cli): update snapshots for the tsrs backend
fengmk2 Nov 5, 2026
0e16dfb
docs(lint): remove backend switching instructions
fengmk2 Nov 6, 2026
7458845
chore(bench): keep Codespaces artifacts out of the PR
fengmk2 Nov 6, 2026
7654142
bench(lint): compare installed dependency disk usage
fengmk2 Nov 6, 2026
c04b21b
docs(lint): record ecosystem compatibility regressions
fengmk2 Nov 6, 2026
7d5d66a
docs(lint): attribute compatibility changes to upstream TypeScript
fengmk2 Nov 8, 2026
2ed6367
chore(deps): update tsrs to 0.23.0-ts7.1.0-beta
fengmk2 Nov 9, 2026
4c263b9
fix(check): align nested projects with whole-program checks
fengmk2 Nov 9, 2026
0a695c6
docs(rfc): describe the tsrs-lint backend replacement
fengmk2 Nov 11, 2026
c5cbe08
chore(deps): update tsrs to 0.24.0-ts7.1.0-beta
fengmk2 Nov 11, 2026
8b1cd9c
docs(rfc): compare lint backend dependencies
fengmk2 Nov 11, 2026
d9f6088
docs(rfc): explain the default tsrs user experience
fengmk2 Nov 11, 2026
e33c377
docs(rfc): simplify the tsrs proposal
fengmk2 Nov 11, 2026
a36541a
test(lint): update inherited type-check snapshots
fengmk2 Nov 11, 2026
6350fb7
feat(lint): ship tsrs as the default backend
fengmk2 Nov 12, 2026
73d4b4f
fix(migrate): scope tsrs trust exception to reviewed release
fengmk2 Nov 12, 2026
25a7c00
chore(bench): keep tsgolint in root development dependencies
fengmk2 Nov 12, 2026
68eb840
docs(bench): report complete fork checks with latest tsrs preview
fengmk2 Nov 12, 2026
9a42bad
docs(bench): refresh local preview timing and memory results
fengmk2 Nov 12, 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
1 change: 1 addition & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"oxlint",
"oxlint-tsgolint",
"tsdown",
"tsrs",
"vite",
"vitest",
"vitest-dev",
Expand Down
5 changes: 5 additions & 0 deletions .github/scripts/__tests__/upgrade-deps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ test.each([false, true])(
if (url === 'https://registry.npmjs.org/oxlint-tsgolint/latest') {
return Response.json({ version: '7.0.2004' });
}
if (url === 'https://registry.npmjs.org/tsrs/latest') {
return Response.json({ version: '0.26.0-ts7.1.0-beta' });
}
if (
url === 'https://registry.npmjs.org/oxc-minify/latest' ||
url === 'https://registry.npmjs.org/oxc-transform/latest'
Expand Down Expand Up @@ -173,6 +176,7 @@ test.each([false, true])(
expect(readFileSync(workspacePath, 'utf8')).toContain("\n '@oxlint/plugins': =1.88.0\n");
expect(readFileSync(workspacePath, 'utf8')).toContain('\n oxlint: =1.88.0\n');
expect(readFileSync(workspacePath, 'utf8')).toContain('\n oxlint-tsgolint: =7.0.2004\n');
expect(readFileSync(workspacePath, 'utf8')).toContain('\n tsrs: =0.26.0-ts7.1.0-beta\n');
expect(readFileSync(nodeVersionPath, 'utf8')).toBe('24.10.2\n');
expect(readFileSync(installerPath, 'utf8')).toContain(
'const PINNED_PNPM_VERSION: &str = "10.40.2";',
Expand Down Expand Up @@ -201,6 +205,7 @@ test.each([false, true])(
expect(versions['@oxlint/plugins'].new).toBe('1.88.0');
expect(versions['@oxlint/plugins'].new).toBe(versions.oxlint.new);
expect(versions['oxlint-tsgolint'].new).toBe('7.0.2004');
expect(versions.tsrs.new).toBe('0.26.0-ts7.1.0-beta');
expect(versions).not.toHaveProperty('oxc-minify');
expect(versions).not.toHaveProperty('oxc-transform');
expect(versions['oxc-node']).toEqual(
Expand Down
11 changes: 11 additions & 0 deletions .github/scripts/upgrade-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type PnpmWorkspaceVersions = {
oxfmt: string;
oxlint: string;
oxlintTsgolint: string;
tsrs: string;
oxcProjectRuntime: string;
oxcProjectTypes: string;
oxcParser: string;
Expand Down Expand Up @@ -369,6 +370,12 @@ async function updatePnpmWorkspace(versions: PnpmWorkspaceVersions): Promise<voi
replacement: `oxlint-tsgolint: =${versions.oxlintTsgolint}`,
newVersion: versions.oxlintTsgolint,
},
{
name: 'tsrs',
pattern: /tsrs: =([\d.]+(?:-[\w.-]+)?)/,
replacement: `tsrs: =${versions.tsrs}`,
newVersion: versions.tsrs,
},
{
name: '@oxc-project/runtime',
pattern: /'@oxc-project\/runtime': =([\d.]+(?:-[\w.]+)?)/,
Expand Down Expand Up @@ -566,6 +573,7 @@ async function upgradeDependencies(): Promise<void> {
oxfmtVersion,
oxlintVersion,
oxlintTsgolintVersion,
tsrsVersion,
oxcProjectRuntimeVersion,
oxcProjectTypesVersion,
oxcParserVersion,
Expand All @@ -582,6 +590,7 @@ async function upgradeDependencies(): Promise<void> {
getLatestNpmVersion('oxfmt'),
getLatestNpmVersion('oxlint'),
getLatestNpmVersion('oxlint-tsgolint'),
getLatestNpmVersion('tsrs'),
getLatestNpmVersion('@oxc-project/runtime'),
getLatestNpmVersion('@oxc-project/types'),
getLatestNpmVersion('oxc-parser'),
Expand All @@ -599,6 +608,7 @@ async function upgradeDependencies(): Promise<void> {
console.log(`oxlint: ${oxlintVersion}`);
console.log(`@oxlint/plugins (from oxlint): ${oxlintVersion}`);
console.log(`oxlint-tsgolint: ${oxlintTsgolintVersion}`);
console.log(`tsrs: ${tsrsVersion}`);
console.log(`@oxc-project/runtime: ${oxcProjectRuntimeVersion}`);
console.log(`@oxc-project/types: ${oxcProjectTypesVersion}`);
console.log(`oxc-parser: ${oxcParserVersion}`);
Expand All @@ -621,6 +631,7 @@ async function upgradeDependencies(): Promise<void> {
oxfmt: oxfmtVersion,
oxlint: oxlintVersion,
oxlintTsgolint: oxlintTsgolintVersion,
tsrs: tsrsVersion,
oxcProjectRuntime: oxcProjectRuntimeVersion,
oxcProjectTypes: oxcProjectTypesVersion,
oxcParser: oxcParserVersion,
Expand Down
295 changes: 295 additions & 0 deletions bench/tsrs-lint-ecosystem.md

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions bench/tsrs-lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Type-aware linting with tsrs-lint

The timing and memory results below were measured locally on **2026-11-13** with
the latest published [PR #3352 preview](https://github.com/voidzero-dev/vite-plus/actions/runs/43194215156),
`0.0.0-commit.25a7c006af2db834bfc66f9689b02de1060f580f`.
Both controls use the same published CLI and Oxlint `1.93.0`: the Go control uses
`oxlint-tsgolint@7.0.2006`, and the default preview uses `tsrs@0.25.0-ts7.1.0-beta`.
The subsequent source commit `68eb84008` changes only reports.

All 12 validation commands and 204 benchmark invocations pass. The backends have
matching checked-file counts and no diagnostics on these two inputs. The separate
[full ecosystem comparison](./tsrs-lint-ecosystem.md) documents compatibility
differences on other projects; these performance results do not establish compiler parity.

## Elapsed time

Each cell shows the median of **10 runs**, followed by the interquartile range
(25th–75th percentile) in brackets. One warm-up per backend is excluded.
Each run starts a fresh CLI process. Backend order alternates between pairs.
Timing runs do not sample memory, and all benchmark commands run sequentially.

| Project | Workload | Go (s) | tsrs (s) | Reduction |
| ------- | -------------------------------- | ------------------: | ------------------: | --------: |
| Vite+ | Type-aware rules | 1.518 [1.489–1.654] | 1.371 [1.255–1.399] | 9.7% |
| Vite+ | Rules and TypeScript diagnostics | 1.861 [1.820–1.928] | 1.410 [1.316–1.504] | 24.2% |
| Vite+ | Full `vp check` | 2.112 [2.102–2.243] | 1.674 [1.646–1.795] | 20.8% |
| vinext | Type-aware rules | 2.677 [2.616–2.791] | 1.572 [1.533–1.656] | 41.3% |
| vinext | Rules and TypeScript diagnostics | 3.364 [3.296–3.454] | 1.695 [1.635–1.728] | 49.6% |
| vinext | Full `vp check` | 4.680 [4.563–4.731] | 2.178 [2.095–2.184] | 53.4% |

## Peak memory

Each cell shows the median of **5 separate runs** and its interquartile range.
One warm-up per backend is excluded. Memory is the sampled peak of the simultaneous
sum of RSS for the CLI and its descendants, including Node, Oxlint, and the native
backend. The sampler itself is excluded. One GiB is 1,073,741,824 bytes.

| Project | Workload | Go (GiB) | tsrs (GiB) | Reduction |
| ------- | -------------------------------- | ------------------: | ------------------: | --------: |
| Vite+ | Type-aware rules | 1.103 [1.047–1.153] | 0.633 [0.601–0.647] | 42.6% |
| Vite+ | Rules and TypeScript diagnostics | 1.276 [1.258–1.462] | 0.651 [0.644–0.670] | 49.0% |
| Vite+ | Full `vp check` | 1.325 [1.269–1.332] | 0.706 [0.687–0.715] | 46.7% |
| vinext | Type-aware rules | 3.427 [3.407–3.632] | 1.852 [1.836–1.889] | 46.0% |
| vinext | Rules and TypeScript diagnostics | 3.694 [3.662–3.904] | 1.967 [1.956–1.977] | 46.8% |
| vinext | Full `vp check` | 5.014 [4.756–5.391] | 2.102 [2.090–2.102] | 58.1% |

The sampler sleeps for 20 ms; the median observed interval is 38.8 ms
(the median of the measured runs' sampling intervals). Shared pages can count more
than once, and sampling can miss short peaks. These are process-tree RSS values,
not total allocations or macOS physical-footprint measurements.

## Inputs and method

| Input | Pin / condition |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Machine | Apple M4 Max, 16 CPU cores, 48 GiB RAM, macOS `27.0.1`, ARM64 |
| Vite+ source | [`68eb84008`](https://github.com/voidzero-dev/vite-plus/tree/68eb84008fd27a72a5e9426ad64636432385b0e1), Node.js `24.22.2` |
| vinext source | [`7f1feb134`](https://github.com/fengmk2/vinext/tree/7f1feb134255f9e816e1b03760b97c10148eed3c), Node.js `26.13.0` |
| Lint threads | `4` for both lint workloads |
| Full check threads | Default `16`, confirmed in format and lint output |
| Repetitions per project/workload/backend | 10 timing runs, 5 memory runs, and one warm-up before each set |

The three workloads are:

```sh
vp lint --type-aware --threads 4 --format json -c <rules-only-config>
vp lint --type-aware --type-check --threads 4 --format json
vp check
```

The temporary rules-only JSON contains the project's `lint` section with
`options.typeCheck` set to `false`; all rules, plugins, and ignore patterns
are retained. Vinext lint uses sorted checker scheduling under both controls.
Full checks and Vite+ lint use default checker scheduling.

Both controls use the same source and installed dependencies. Vite+ retains its
own workspace dependency links; the measured CLI, Oxlint, and native backends
come from the published preview. Vinext links the preview CLI and core into its
workspace and pnpm peer importers. These temporary links are restored afterwards.
All commands use isolated temporary `VP_HOME` directories and disable the Node
compile cache. Setup, configuration extraction, and output validation are excluded
from the timed intervals; CLI startup and command execution are included.

Native executable paths are verified in every validation and memory run. Package
versions, executable hashes, source commits, and lockfile hashes are recorded.
Tracked project sources and lockfiles are unchanged after measurement.

| Project | Linted files | Rules | Formatted files in `vp check` |
| ------- | -----------: | ----: | ----------------------------: |
| Vite+ | 387 | 174 | 675 |
| vinext | 1,842 | 151 | 4,755 |

## Interpretation and reproducibility

This is a local desktop measurement with warm filesystem caches, not a cold-cache
or isolated-server result. CPU use measured for 0.5 seconds before each timed run
has a median of 12.4% across the machine (range 7.2%–23.9%).
The CPU frequency is not fixed. Compare the paired backends within each row;
the interquartile ranges describe sample spread, not confidence intervals.

The vinext input contains both the Sentry fixture exclusion from its draft PR
and the upstream [`prefer-shared-utils` optimization](https://github.com/cloudflare/vinext/commit/5a6cc15f39ca7e93bb1892efdcf2d3d01aa1b562).
Its original plugin rules remain enabled. These source inputs, the hardware,
thread counts, and tool versions differ from the earlier Codespaces measurements.
Changes in absolute times or reduction percentages cannot be attributed solely
to the tsrs version update.

All measured samples are retained; there is no outlier filtering. Reductions use
`100 * (1 - tsrs median / Go median)`. Raw samples, commands, memory traces,
dependency receipts, and the local runner remain outside the repository.

## Historical installed disk footprint

The following disk measurements retain the **2026-11-06 Linux x64** comparison
of published `1.5.0` and the `0.22.0` preview. They are not measurements of the
current macOS preview. The [original report](https://github.com/voidzero-dev/vite-plus/blob/68eb84008fd27a72a5e9426ad64636432385b0e1/bench/tsrs-lint.md#installed-disk-footprint)
contains the installation method, package inventory scope, and lockfile hashes.

| Scope | Go (MiB) | tsrs (MiB) | Increase |
| --------------------------------------------- | -------: | ---------: | -------: |
| Complete `node_modules`, on disk | 132.93 | 150.87 | 13.5% |
| Complete `node_modules`, regular-file lengths | 123.39 | 139.27 | 12.9% |
| Backend packages, on disk | 21.63 | 39.69 | 83.5% |

One MiB is 1,048,576 bytes. The total installation comparison includes other
release-to-preview dependency changes; the backend row isolates the backend
packages. These disk figures do not describe runtime memory use.
95 changes: 95 additions & 0 deletions bench/tsrs-lint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Run from the workspace root with OXLINT_TSGOLINT_PATH pointing to tsrs-lint.
// Additional arguments (e.g. --type-check or file paths) go to both vp lint runs.
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';

const { OXLINT_TSGOLINT_PATH: tsrs, ...env } = process.env;
if (!tsrs) {
throw new Error('Set OXLINT_TSGOLINT_PATH to the tsrs-lint executable.');
}
const vp = fileURLToPath(new URL('../packages/cli/bin/vp', import.meta.url));
const require = createRequire(import.meta.url);
const tsgolint =
process.platform === 'win32'
? ['tsgolint.exe', 'tsgolint.cmd']
.map((bin) => fileURLToPath(new URL(`../node_modules/.bin/${bin}`, import.meta.url)))
.find(existsSync)
: require.resolve('oxlint-tsgolint/bin/tsgolint');
assert.ok(tsgolint, 'Install workspace development dependencies to get the Go baseline.');
const baselineEnv = {
...env,
OXLINT_TSGOLINT_PATH: tsgolint,
};
const args = [
vp,
'lint',
'--type-aware',
'--threads',
'4',
'--format',
'json',
...process.argv.slice(2),
];

function run(env: NodeJS.ProcessEnv) {
const start = performance.now();
const { status, stdout, stderr, error } = spawnSync(process.execPath, args, {
env,
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
const seconds = (performance.now() - start) / 1000;
if (error) {
throw error;
}
if ((status !== 0 && status !== 1) || !stdout.trimStart().startsWith('{')) {
throw new Error(`vp lint exited with ${status}\n${stderr}\n${stdout}`);
}
const report = JSON.parse(stdout) as {
diagnostics: unknown[];
number_of_files: number;
number_of_rules: number;
};
assert.ok(report.number_of_files > 0, 'Expected vp lint to analyze files');
return {
seconds,
result: {
status,
files: report.number_of_files,
rules: report.number_of_rules,
diagnostics: report.diagnostics.map((d) => JSON.stringify(d)).toSorted(),
},
};
}

const backends = [
{ name: 'tsgolint', env: baselineEnv, samples: [] as number[] },
{
name: 'tsrs-lint',
env: { ...baselineEnv, OXLINT_TSGOLINT_PATH: tsrs },
samples: [] as number[],
},
];
const [expected, candidate] = backends.map(({ env }) => run(env).result);
assert.deepEqual(candidate, expected);
const pairs = 10;
for (let pair = 0; pair < pairs; pair++) {
for (const backend of pair % 2 === 0 ? backends : backends.toReversed()) {
const { result, seconds } = run(backend.env);
assert.deepEqual(result, expected);
backend.samples.push(seconds);
}
}
console.log(
`${expected.files} files, ${expected.rules} rules, ${expected.diagnostics.length} diagnostics, exit ${expected.status}`,
);
for (const { name, samples } of backends) {
const sorted = samples.toSorted((a, b) => a - b);
const median = (sorted[pairs / 2 - 1] + sorted[pairs / 2]) / 2;
console.log(
`${name} median ${median.toFixed(3)} s [${samples.map((s) => s.toFixed(3)).join(', ')}]`,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ Local vite-plus:
vite-plus <version>

Tools:
vite <version>
rolldown <version>
vitest <version>
oxfmt <version>
oxlint <version>
oxlint-tsgolint <version>
tsdown <version>
vite <version>
rolldown <version>
vitest <version>
oxfmt <version>
oxlint <version>
tsrs <version>
tsdown <version>

Environment:
Package manager Not found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ Vite+ toolchain (local)

vite-plus@<version>
├── depends on @voidzero-dev/vite-plus-core@<version>
├── depends on oxlint-tsgolint@<version>
├── depends on tsrs@<version>
└── compiles vite-task (built <build-time>, revision <revision>)
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vite-plus@<version>
│ └── bundles tsdown@<version>
├── depends on vitest@<version>
├── depends on oxlint@<version>
├── depends on oxlint-tsgolint@<version>
├── depends on tsrs@<version>
├── depends on oxfmt@<version>
├── compiles vite-task (built <build-time>, revision <revision>)
└── compiles oxc-node@<version> (<revision>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vite-plus@<version>
│ └── bundles tsdown@<version>
├── depends on vitest@<version>
├── depends on oxlint@<version>
├── depends on oxlint-tsgolint@<version>
├── depends on tsrs@<version>
├── depends on oxfmt@<version>
├── compiles vite-task (built <build-time>, revision <revision>)
└── compiles oxc-node@<version> (<revision>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ vite-plus@<version>
│ └── bundles tsdown@<version>
├── depends on vitest@<version>
├── depends on oxlint@<version>
├── depends on oxlint-tsgolint@<version>
├── depends on tsrs@<version>
├── depends on oxfmt@<version>
├── compiles vite-task (built <build-time>, revision <revision>)
└── compiles oxc-node@<version> (<revision>)
Expand Down Expand Up @@ -85,7 +85,7 @@ vite-plus@<version>
│ └── bundles tsdown@<version>
├── depends on vitest@<version>
├── depends on oxlint@<version>
├── depends on oxlint-tsgolint@<version>
├── depends on tsrs@<version>
├── depends on oxfmt@<version>
├── compiles vite-task (built <build-time>, revision <revision>)
└── compiles oxc-node@<version> (<revision>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Local vite-plus:
vite-plus <version>

Tools:
vite <version>
rolldown <version>
vitest <version>
oxfmt <version>
oxlint <version>
oxlint-tsgolint <version>
tsdown <version>
vite <version>
rolldown <version>
vitest <version>
oxfmt <version>
oxlint <version>
tsrs <version>
tsdown <version>
```
Loading
Loading