Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ jobs:
- name: CLI integration
run: node packages/cli/test/integration.test.js

# CLI: tsgo backend + PoC smoke (requires @typescript/native-preview).
- name: CLI tsgo backend
run: |
node -e "const {hasNativePreview}=require('./packages/cli/lib/tsgo-load.js'); if(!hasNativePreview()){console.error('@typescript/native-preview not installed'); process.exit(1)}"
node packages/cli/test/tsgo-backend.test.js
pnpm -C packages/poc-tsgo run build
node packages/poc-tsgo/test/poc.test.js

# CLI: layer 2 cross-session affected-file diff.
- name: CLI incremental state
run: node packages/cli/test/incremental-state.test.js
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ TSLint (TS-AST, deprecated 2019) → ESLint took over via `typescript-eslint`
|---|---|---|---|---|
| Runtime | Node, separate process | Node, separate process | Rust, separate process | Node, in `tsserver` |
| AST | ESTree | TS AST | Native Rust AST | TS AST |
| Type-aware rules | Yes (its own `Program`) | Yes (its own `Program`) | Yes (via `tsgolint`, alpha) | Yes (shared `TypeChecker`) |
| Type-aware rules | Yes (its own `Program`) | Yes (its own `Program`) | Yes (via `tsgolint`, alpha) | Yes (shared `TypeChecker`; CLI `--tsgo` beta uses ts-go shim) |
| Built-in rules | Many | Deprecated | Subset of ESLint (+ JS plugins, alpha) | Zero (imports ESLint / TSLint / TSL) |
| Status | Active standard | Deprecated 2019 | Active | Active |

Expand Down Expand Up @@ -211,10 +211,36 @@ Flags:
| `--fix` | Apply fixes |
| `--force` | Ignore cache |
| `--failures-only` | Only print diagnostics that affect exit code |
| `--tsgo` | Use `@typescript/native-preview` (ts-go) as the type backend — plain `--project` only; see [ts-go backend](#ts-go-backend-cli) |
| `--tsgo-fast` | With `--tsgo`: always use the fast path (skip disk cache, eager-prepare) |
| `--no-tsgo-fast` | With `--tsgo`: disable auto fast path on multi-file projects |
| `-h`, `--help` | |

TSSLint produces diagnostics and edits — it does not format. Run dprint or Prettier after `--fix`.

### ts-go backend (CLI)

The default CLI path uses the Node `typescript` package (**Strada**). For plain TypeScript projects you can opt into the Go-based compiler via [`@typescript/native-preview`](https://www.npmjs.com/package/@typescript/native-preview):

```bash
npm install @typescript/native-preview --save-dev # optional peer of @tsslint/cli
npx tsslint --project tsconfig.json --tsgo
```

| | Strada (default) | `--tsgo` |
|---|---|---|
| Runtime | Node `typescript` in-process | `tsgo` child process + TypeScript API shim |
| Framework flags | Vue / MDX / Astro / … | Not supported — `--project` only |
| Multi-file | Layer 1 + 2 disk cache | Skip layer-1 cache; **one tsgo child** reused across `--project` entries via `updateSnapshot` |
| `--tsgo-fast` | — | Also eager-prepare all files at setup (opt-in; can regress type-heavy runs) |
| Editor plugin | N/A (CLI) | N/A — `tsserver` plugin still requires Strada today |

On this repo (ts-eslint type-aware, ~37 files in `packages/{cli,core,config}`): Strada ~1.6–3s, `--tsgo` ~1.8–2s. Full monorepo (~59 files, 8 tsconfigs): Strada ~4–8s, `--tsgo` ~6–11s (IPC-bound on compat-eslint); shared child + lazy prepare helps multi-`--project` runs.

Dogfood (`pnpm run lint` vs `pnpm run lint:tsgo -- --force` on this monorepo): Strada **76 passed** / 1 message; `--tsgo` **68 passed** / 24 messages. Shim sources (`tsgo-*.ts`) are scoped out of `no-unnecessary-type-assertion` (tsgo vs Strada checker disagree on assertion necessity). Remaining gap is the same rule firing on other files under the tsgo checker only.

Rules still author against the TypeScript compiler API; the shim translates ts-go's checker into `ts.Program` / `ts.TypeChecker` shapes. See `packages/poc-tsgo/` for a minimal parity/benchmark harness (`pnpm run poc:tsgo`, `pnpm run bench:tsgo`).

## Framework support

The `--*-project` flags wire in [Volar](https://volarjs.dev/) language plugins so framework files (Vue SFCs, MDX, Astro components, etc.) are virtualized as TypeScript before linting. Anything `tsserver` can see, TSSLint can lint.
Expand Down Expand Up @@ -336,7 +362,8 @@ Build your own with the `Plugin` type from `@tsslint/types`.

- Node.js **22.6.0+** (uses `--experimental-strip-types` to load `tsslint.config.ts` directly — no transpile step)
- Any TypeScript version with Language Service Plugin support
- Not compatible with `typescript-go` (v7), which does not yet support Language Service Plugins
- **`@tsslint/typescript-plugin` (editor)**: requires the Node `typescript` package — not compatible with `typescript-go` / ts-go, which does not yet support Language Service Plugins
- **`@tsslint/cli --tsgo` (beta)**: optional `@typescript/native-preview` peer for the Go compiler backend on plain `--project` runs; see [ts-go backend](#ts-go-backend-cli)

## License

Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
"start": "packages/cli/bin/tsslint.js",
"format": "dprint fmt",
"lint": "packages/cli/bin/tsslint.js --project {tsconfig.json,packages/*/tsconfig.json}",
"lint:tsgo": "packages/cli/bin/tsslint.js --project {tsconfig.json,packages/*/tsconfig.json} --tsgo --force",
"lint:fix": "npm run lint -- --fix && npm run format",
"lint:fixtures": "packages/cli/bin/tsslint.js --project fixtures/*/tsconfig.json --vue-project fixtures/meta-frameworks/tsconfig.json --mdx-project fixtures/meta-frameworks/tsconfig.json --astro-project fixtures/meta-frameworks/tsconfig.json --ts-macro-project fixtures/meta-frameworks/tsconfig.json"
"lint:fixtures": "packages/cli/bin/tsslint.js --project fixtures/*/tsconfig.json --vue-project fixtures/meta-frameworks/tsconfig.json --mdx-project fixtures/meta-frameworks/tsconfig.json --astro-project fixtures/meta-frameworks/tsconfig.json --ts-macro-project fixtures/meta-frameworks/tsconfig.json",
"poc:tsgo": "pnpm -C packages/poc-tsgo run build && node packages/poc-tsgo/run-poc.js",
"bench:tsgo": "pnpm -C packages/poc-tsgo run build && node packages/poc-tsgo/bench.js"
},
"devDependencies": {
"@lerna-lite/cli": "latest",
"@lerna-lite/publish": "latest",
"@types/node": "latest",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript/native-preview": "7.0.0-dev.20260624.1",
"dprint": "latest",
"eslint-plugin-react-x": "^4.2.3",
"typescript": "latest"
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,22 @@ npx tsslint --project 'packages/*/tsconfig.json' --filter 'src/**/*.ts'

Run `tsslint --help` for the full flag list.

### Beta: `--tsgo` (TypeScript native / ts-go backend)

Use the Go-based TypeScript compiler (`@typescript/native-preview`) instead of the Node `typescript` package:

```bash
npx tsslint --project tsconfig.json --tsgo
```

- Requires optional peer `@typescript/native-preview` (pinned in this package).
- Plain `--project` only — no Vue / MDX / Astro framework flags.
- Multi-file `--tsgo` skips layer-1 disk cache and reuses **one tsgo child** across `--project` entries. `--tsgo-fast` additionally eager-prepares all files at setup (opt-in).
- Benchmark on this repo (~37 ts-eslint files): Strada ~1.6–3s, `--tsgo` ~1.6–2s; full repo (~59 files, 8 tsconfigs): Strada ~1.8–2s, `--tsgo` ~2.1s (~1.05× Strada).
- Dogfood parity: `pnpm run lint:tsgo` — 68/76 files clean vs Strada (shim sources exempt from `no-unnecessary-type-assertion`; remaining gap is tsgo checker divergence on other files).

```bash
pnpm run lint:tsgo # same projects as lint, with --tsgo --force
```

See the [root README](../../README.md) for framework project flags (`--vue-project`, `--mdx-project`, …), caching behavior, and how diagnostics are emitted.
32 changes: 22 additions & 10 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import minimatch = require('minimatch');
import languagePlugins = require('./lib/languagePlugins.js');
import colors = require('./lib/colors.js');
import render = require('./lib/render.js');
import tsgoMode = require('./lib/tsgo-mode.js');

const HELP = `
Usage: tsslint [options]
Expand All @@ -35,6 +36,9 @@ Options:
--failures-only Only print errors and messages (skip warnings and suggestions)
--list-rules After linting, print each rule's classification (syntactic / type-aware)
--debug-estree After linting, print the actual ESTree node types converted by @tsslint/compat-eslint and their counts
--tsgo Use @typescript/native-preview as the type backend (beta; plain --project only)
--tsgo-fast With --tsgo: skip disk cache + eager-prepare all files at setup
--no-tsgo-fast With --tsgo: keep disk cache on multi-file runs (default prepare is lazy)
-h, --help Show this help message

Examples:
Expand Down Expand Up @@ -160,7 +164,7 @@ class Project {
// key includes tsslint version, TS version, tsconfig, languages,
// and configFile mtime+size — anything that changes the rule set or
// the toolchain mints a fresh file. See packages/cli/lib/cache.ts.
if (!process.argv.includes('--force')) {
if (!process.argv.includes('--force') && !tsgoMode.shouldTsgoFast(this.fileNames.length)) {
this.cacheData = cache.loadCache(
this.tsconfig,
this.configFile,
Expand Down Expand Up @@ -203,6 +207,10 @@ const formatHost: ts.FormatDiagnosticsHost = {
let configErrors = 0;
const failuresOnly = process.argv.includes('--failures-only');

if (tsgoMode.isTsgoFastExplicit() && !tsgoMode.isTsgoEnabled()) {
fail('--tsgo-fast requires --tsgo.');
}

if (!PROJECT_FLAGS.some(({ flag }) => process.argv.includes(flag))) {
renderer.dispose();
console.log(HELP);
Expand Down Expand Up @@ -281,7 +289,9 @@ const formatHost: ts.FormatDiagnosticsHost = {
process.exit(1);
}

await startWorker(worker.create());
const linterWorker = worker.create();
await startWorker(linterWorker);
linterWorker.shutdown();

const summaryLines: string[] = [];

Expand Down Expand Up @@ -569,14 +579,16 @@ const formatHost: ts.FormatDiagnosticsHost = {
// affected files. Falls through to `undefined` on layer-1-only
// runs, matching the schema contract.
project.cacheData.incrementalState = await linterWorker.buildIncrementalState();
cache.saveCache(
project.tsconfig,
project.configFile!,
project.languages,
ts.version,
project.cacheData,
ts.sys.createHash,
);
if (!tsgoMode.shouldTsgoFast(project.fileNames.length)) {
cache.saveCache(
project.tsconfig,
project.configFile!,
project.languages,
ts.version,
project.cacheData,
ts.sys.createHash,
);
}

await startWorker(linterWorker);
}
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/lib/real-ts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Captures the real `typescript` module reference BEFORE the tsgo facade
// installs its `Module._resolveFilename` hook. Imported at worker top-level
// so the cache entry is the genuine ts module; subsequent imports of this
// file from anywhere (including code that runs after the facade installs)
// receive the captured-at-load reference unchanged.
//
// Use this from any internal CLI code that needs real ts behaviour
// (parser, binder, scanner) — `require('typescript')` from those callsites
// would otherwise hit the facade and return the tsgo-shaped substitute.
import ts = require('typescript');
export = ts;
38 changes: 38 additions & 0 deletions packages/cli/lib/tsgo-api-pool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// One tsgo child process per CLI worker. Multi-`--project` runs call
// `updateSnapshot({ openProject })` on the same API instead of spawn +
// teardown per tsconfig.

import { loadTsgoModules } from './tsgo-load.js';

type TsgoSync = ReturnType<typeof loadTsgoModules>['sync'];
type TsgoAPI = InstanceType<TsgoSync['API']>;

let sharedApi: TsgoAPI | undefined;
let beforeExitHooked = false;

function ensureBeforeExitHook(): void {
if (beforeExitHooked) return;
beforeExitHooked = true;
process.once('beforeExit', () => {
closeSharedTsgoApi();
});
}

/** Lazily spawn tsgo once; reuse until `closeSharedTsgoApi`. */
export function acquireSharedTsgoApi(): TsgoAPI {
if (!sharedApi) {
const { sync } = loadTsgoModules();
sharedApi = new sync.API({});
ensureBeforeExitHook();
}
return sharedApi;
}

export function closeSharedTsgoApi(): void {
sharedApi?.close();
sharedApi = undefined;
}

export function hasSharedTsgoApi(): boolean {
return sharedApi != null;
}
Loading
Loading