Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eaad28d
docs(state): boat notes for the T3 Code drop-in goal
t3dotgg Oct 21, 2026
6a853cd
perf(incremental): skip the d.ts walk for wide noEmit changes
t3dotgg Oct 21, 2026
b2ceb44
docs(state): T3 drop-in lane results
t3dotgg Oct 21, 2026
9c84b46
perf(incremental): limit the noEmit d.ts fallback by program size
t3dotgg Oct 21, 2026
56bdf92
feat(npm): @tsc-rs/typescript drop-in package
t3dotgg Oct 21, 2026
c2f039d
docs(npm): postinstall approval for npm 12 and bun; pnpm tsdk path
t3dotgg Oct 21, 2026
6c17258
fix(npm): long-path prefix in the tsc-rs getExePath, as Go's
t3dotgg Oct 21, 2026
ef4aa8b
docs(state): T3 drop-in evidence and verdicts
t3dotgg Oct 21, 2026
b3b7351
fix(incremental): keep Go's pending emit in the noEmit d.ts fallback
t3dotgg Oct 21, 2026
9ea51e0
perf(incremental): less loading-thread work before the check of a wid…
t3dotgg Oct 21, 2026
99f37f9
fix(incremental): compute Go's d.ts signatures on a global-scope change
t3dotgg Oct 21, 2026
746bec0
refactor(incremental): referenced-by lists in one pass again
t3dotgg Oct 21, 2026
cbcab76
Merge feat/npm-typescript-dropin (#96): @tsc-rs/typescript drop-in pa…
t3dotgg Oct 21, 2026
a976bc7
Merge goport-t3inc2: noEmit d.ts fallback and less work before a wide…
t3dotgg Oct 21, 2026
981d601
Merge goport-t3inc3: Go's pending emit and global-scope signatures in…
t3dotgg Oct 21, 2026
1bdbb0f
Merge goport-t3inc3: no own-signature rule near a global-scope file
t3dotgg Oct 21, 2026
9577dce
fix(incremental): no own-signature rule near a global-scope file
t3dotgg Oct 21, 2026
aae1956
docs: tsc-rs as the typescript package in T3 Code
t3dotgg Oct 21, 2026
3360fd9
test(incremental): warm up a.ts before the near-global edit
t3dotgg Oct 21, 2026
558199b
Merge goport-t3inc3: warm-up step in the near-global test
t3dotgg Oct 21, 2026
3e1bfb3
docs(incremental): fallback notes match round 3 and the T3 checks
t3dotgg Oct 21, 2026
da467b9
Merge goport-t3inc3: fallback notes
t3dotgg Oct 21, 2026
badee5c
docs(incremental): global-scope closures and the d.ts errors pass
t3dotgg Oct 21, 2026
965afd3
Merge goport-t3inc3: fallback doc fixes
t3dotgg Oct 21, 2026
7594a04
docs(state): T3 drop-in goal results
t3dotgg Oct 21, 2026
6f20276
docs(state): Theo approves the 0.8.0 release
t3dotgg Oct 21, 2026
a0c5e7e
Merge the state notes of the T3 drop-in work
t3dotgg Oct 21, 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
63 changes: 51 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Builds, packs and tests the tsc-rs release: the npm packages tsc-rs, @tsc-rs/linux-x64,
# @tsc-rs/linux-arm64, @tsc-rs/darwin-arm64, @tsc-rs/darwin-x64, @tsc-rs/win32-x64 and
# @tsc-rs/win32-arm64, and one standalone archive per platform (tsc and the lib files; a .zip with
# tsc.exe for Windows).
# Builds, packs and tests the tsc-rs release: the npm packages tsc-rs, @tsc-rs/typescript (the
# drop-in for `typescript`), @tsc-rs/linux-x64, @tsc-rs/linux-arm64, @tsc-rs/darwin-arm64,
# @tsc-rs/darwin-x64, @tsc-rs/win32-x64 and @tsc-rs/win32-arm64, and one standalone archive per
# platform (tsc and the lib files; a .zip with tsc.exe for Windows). Both main packages use the same
# platform packages.
#
# A tag v<version> also publishes the npm packages and creates a GitHub release. A stable version
# (v0.1.0) goes to the dist-tag `latest` and a normal release. A prerelease version (v0.2.0-beta.1)
# goes to `next` and a GitHub prerelease. npm trusted publishing (OIDC) authenticates the
# publish: no npm token. Each of the 7 packages trusts this workflow file and the environment `npm`
# publish: no npm token. Each of the 8 packages trusts this workflow file and the environment `npm`
# (npm/README.md, "Trusted publishing"). Pull requests that change the release files, and a manual
# run, do everything but the release and the publish.
#
Expand Down Expand Up @@ -270,11 +271,19 @@ jobs:
run: |
bins="$RUNNER_TEMP/bins"
chmod +x "$bins"/tsc-*/tsc "$bins"/tsc-*/tsc.exe
scripts/goport/npm-pack.sh --name tsc-rs --package-version "$VERSION" \
--also "linux-arm64=$bins/tsc-linux-arm64/tsc" --also "darwin-arm64=$bins/tsc-darwin-arm64/tsc" \
--also "darwin-x64=$bins/tsc-darwin-x64/tsc" \
--also "win32-x64=$bins/tsc-win32-x64/tsc.exe" --also "win32-arm64=$bins/tsc-win32-arm64/tsc.exe" \
also=(--also "linux-arm64=$bins/tsc-linux-arm64/tsc" --also "darwin-arm64=$bins/tsc-darwin-arm64/tsc"
--also "darwin-x64=$bins/tsc-darwin-x64/tsc"
--also "win32-x64=$bins/tsc-win32-x64/tsc.exe" --also "win32-arm64=$bins/tsc-win32-arm64/tsc.exe")
scripts/goport/npm-pack.sh --name tsc-rs --package-version "$VERSION" "${also[@]}" \
"$RUNNER_TEMP/pkg" "$bins/tsc-linux-x64/tsc"
# The drop-in for `typescript` (npm/README.md). Its optionalDependencies alias the same
# platform packages, which the release publishes once, so they must be the same bytes.
scripts/goport/npm-pack.sh --name @tsc-rs/typescript --package-version "$VERSION" "${also[@]}" \
"$RUNNER_TEMP/pkg-ts" "$bins/tsc-linux-x64/tsc"
for platform in linux-x64 linux-arm64 darwin-arm64 darwin-x64 win32-x64 win32-arm64; do
cmp "$RUNNER_TEMP/pkg/tsc-rs-$platform-$VERSION.tgz" "$RUNNER_TEMP/pkg-ts/tsc-rs-$platform-$VERSION.tgz"
done
cp "$RUNNER_TEMP/pkg-ts/tsc-rs-typescript-$VERSION.tgz" "$RUNNER_TEMP/pkg/"
# One archive per platform: the platform package's lib dir (tsc and the lib files, which a
# noembed tsc reads next to it), LICENSE and NOTICE.txt. A .tar.gz, and a .zip for Windows.
- name: Archives
Expand Down Expand Up @@ -365,6 +374,34 @@ jobs:
npx tsc-rs --outDir out --declaration ok.ts
cat out/ok.js out/ok.d.ts
grep -q "export const x = 1;" out/ok.js
# The drop-in under the name `typescript`, and its platform package under Go's name, as
# "typescript": "npm:@tsc-rs/typescript@<v>" installs them from npm. The tarballs are copied in:
# a relative file: spec has no drive letter on Windows.
- name: npm install and check (@tsc-rs/typescript as typescript)
run: |
platform=${{ matrix.platform }}
mkdir proj-ts && cd proj-ts
cp "$RUNNER_TEMP/dist/tsc-rs-$platform-$VERSION.tgz" "$RUNNER_TEMP/dist/tsc-rs-typescript-$VERSION.tgz" .
echo '{"name":"smoke-ts","private":true}' > package.json
npm install --offline --no-audit --no-fund "@typescript/typescript-$platform@file:./tsc-rs-$platform-$VERSION.tgz" \
"typescript@file:./tsc-rs-typescript-$VERSION.tgz"
# The tsc and require("typescript").version report the TypeScript version (tscVersion).
tsc_version=$(node -p 'require("./node_modules/typescript/package.json").tscVersion')
npx tsc --version
[[ $(npx tsc --version) == "Version $tsc_version" ]]
node -p 'require("typescript").version'
[[ $(node -p 'require("typescript").version') == "$tsc_version" ]]
node --input-type=module -e 'await import("typescript/unstable/sync")'
echo 'const x: number = "a";' > bad.ts
set +e
npx tsc --noEmit bad.ts > check.txt
rc=$?
set -e
cat check.txt
[[ $rc == 2 ]] && grep -q "error TS2322" check.txt
echo 'export const x: number = 1;' > ok.ts
npx tsc --outDir out --declaration ok.ts
grep -q "export const x = 1;" out/ok.js
- name: Archive check
if: runner.os != 'Windows'
run: |
Expand Down Expand Up @@ -403,7 +440,7 @@ jobs:
with:
name: release
path: dist
# Platform packages first, then the main package that depends on them. A version that is
# Platform packages first, then the main packages that depend on them. A version that is
# already on npm is skipped, so a failed run can run again. While the repo is private, npm
# makes no provenance; from a public repo it adds it by itself.
# A version with a prerelease part (a "-") is a prerelease: dist-tag `next`. Else `latest`.
Expand All @@ -414,7 +451,8 @@ jobs:
echo "dist-tag $tag"
for pkg in @tsc-rs/linux-x64:tsc-rs-linux-x64 @tsc-rs/linux-arm64:tsc-rs-linux-arm64 \
@tsc-rs/darwin-arm64:tsc-rs-darwin-arm64 @tsc-rs/darwin-x64:tsc-rs-darwin-x64 \
@tsc-rs/win32-x64:tsc-rs-win32-x64 @tsc-rs/win32-arm64:tsc-rs-win32-arm64 tsc-rs:tsc-rs; do
@tsc-rs/win32-x64:tsc-rs-win32-x64 @tsc-rs/win32-arm64:tsc-rs-win32-arm64 tsc-rs:tsc-rs \
@tsc-rs/typescript:tsc-rs-typescript; do
name=${pkg%%:*} file=./dist/${pkg#*:}-$VERSION.tgz
[[ -f $file ]] || { echo "::error::missing package $file"; exit 1; }
if npm view "$name@$VERSION" version > /dev/null 2>&1; then
Expand All @@ -429,6 +467,7 @@ jobs:
run: |
pre=() install="npm install -D tsc-rs"
[[ $VERSION != *-* ]] || pre=(--prerelease) install="npm install -D tsc-rs@next"
dropin="\"typescript\": \"npm:@tsc-rs/typescript@$VERSION\""
gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" "${pre[@]}" \
--title "tsc-rs $VERSION" --generate-notes \
--notes "Install with \`$install\`, or download the archive for your platform."
--notes "Install with \`$install\`, use it as the \`typescript\` package with \`$dropin\` in package.json, or download the archive for your platform."
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,22 @@ npx tsc-rs -p tsconfig.json
```

`tsc-rs` takes the same options as `tsc`. The npm package is `tsc-rs` so that it does not clash
with the `typescript` package. Each [release](https://github.com/pingdotgg/ts-rust/releases) also
has a standalone archive per platform: the `tsc` binary with the lib files next to it.
with the `typescript` package. To use tsc-rs as the `typescript` package (its `tsc` bin, its JS API
and the TypeScript 7 VS Code extension), install
[@tsc-rs/typescript](npm/typescript-readme.md) under that name (from the first release after
0.7.0):

```json
{ "devDependencies": { "typescript": "npm:@tsc-rs/typescript@<version>" } }
```

Each [release](https://github.com/pingdotgg/ts-rust/releases) also has a standalone archive per
platform: the `tsc` binary with the lib files next to it.

Platforms: Linux x64 and arm64 (static, any distribution), macOS arm64, and (from the first
release after 0.2.0) macOS x64, Windows x64 and Windows arm64.

To use it in VS Code, see the [npm package README](npm/tsc-rs-readme.md#vs-code).
To use it in VS Code, see [@tsc-rs/typescript](npm/typescript-readme.md#vs-code).

## Effect diagnostics

Expand Down
107 changes: 107 additions & 0 deletions crates/ts_goport/PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,103 @@ The batch that adds it is not accepted until Theo approves.
output where Go reads new in 6 of 6 runs (probe q1b2,
k2sched/design-a-check.md). Go runs each status check on the builder
that takes the task.
- An incremental `noEmit` program of more than 32 files skips Go's d.ts
walk (t3inc1, not in Go; `incremental/affected_files.rs`
`skips_dts_walk`). When a changed file gets a new d.ts signature, Go's
`getFilesAffectedBy` emits the d.ts of each importer, one at a time, to
see if its signature changed too. After a fresh build every stored
signature is a version, so the walk goes through the whole importer
closure. On T3 Code apps/server one edit made 675 serial d.ts emits, and
the incremental run took 1.6x the cold run (Go at the pin: 1.4x). A small
closure can cost as much: in T3 Code packages/contracts an edit of a file
with 11 importers made 11 d.ts emits that took 1.0 s, against 0.35 s for
the cold run, because printing the inferred Effect Schema types costs
more than checking them. The walk does not change which files are
checked again: Go removes the cached diagnostics of the whole closure
anyway. Under `noEmit` it sets only the stored signatures and the full
pending emit kind of the files that it visits. So under `noEmit`, without
`assumeChangesOnlyAffectDirectDependencies`, in a program with more than
32 source files that are not declaration or JSON files, two rules apply
(t3inc1, t3inc3):
- Walk skip: each file of the closure of a changed file with a new d.ts
signature is affected and stores its version as signature, with no
d.ts emit. This gives the full emit kind to each file of the closure.
Go gives it to the files that its walk visits, always the direct
importers, and gives the d.ts kind of `handleDtsMayChangeOf` (none
without declarations) to the deeper files. So the rule applies only
when each deeper file that is not a changed file is already pending the
JS part of the full kind (`walk_skip_keeps_pending_emit`).
- Own signature: when more than 32 files import the changed file (directly
or not), its own d.ts signature is still computed: when it did not change,
only that file is affected, as in Go. With 32 or fewer, the changed file
uses its version too, so its closure is checked again with no d.ts emit,
unless it or a file of its closure affects the global scope (the version
would then make every file of the program affected, where Go's d.ts emit
may find no change and affect only the changed file) or its stored
signature is its version (then its text did not change, and Go's emit
decides). Go makes only the changed file pending when its d.ts did not
change, so the rule applies only when each file of the closure that is not
a changed file is already pending the full kind. In T3 Code
packages/contracts the d.ts emit of orchestrationV2.ts alone took 0.63 s,
against 0.36 s for a cold run: two of its inferred types exceed the length
that the compiler serializes (TS7056).

A fresh build makes every file pending with the full kind. A `noEmit`
build emits no JS, and with declarations it clears only the d.ts errors
part, when its d.ts errors pass runs: Go runs it only when the run reports
no other diagnostic (program.go:2082). So in a project that never emits,
the walk skip always applies. The own signature rule applies when the
project has no declarations, and with declarations while the d.ts errors
part stays pending (T3 Code apps/web, `composite`, keeps the full kind on
every file; its runs report Effect suggestions). After an emitting build
both give way to Go's walk until the files are pending again.

In the run that takes a rule, the output, the diagnostics and the pending
emit are Go's. The build info differs from Go's only in what the skipped
d.ts emits give Go: in `fileInfos` these files store versions where Go
stores d.ts hashes, and `packageJsons` and `missingPackageJsons` lack the
package.json files that only those emits look up for module specifiers.
Both are what a fresh build stores. The package.json lists only feed the
up-to-date checks of `tsc -b` and watch. A change that affects the global
scope makes every file compute its d.ts signature, as in Go, except the
files that a rule gave their version in the same run (T3 Code apps/web, a
global edit and a contracts edit in one run: 1,447 files, and 1 of 2,095
missing package.json files).

In a later run, a change of a file that stores its version and keeps its
d.ts counts as a new signature, so its closure is checked again. Go checks
only that file. When a file of that closure affects the global scope,
every file is checked again, still under a cold run. When the changed file
itself affects the global scope, every file also computes its d.ts
signature (Go's global path): on T3 Code apps/web, after an export change
of a utils.ts that skips the walk (2.40 s; Go's walk 4.70 s), the next
body edit of a file with `declare global` took 4.74 s (Go's walk 0.29 s).
The diagnostics stay Go's. When an emitting build without declarations
came in between (one with declarations stores the d.ts hashes again), that
later run takes Go's walk from the versions, so `affectedFilesPendingEmit`
also holds the files that import the changed file, and the next emitting
build writes them again (the same bytes). Go writes only that file. In the
t3inc3 matrix of 5 option sets and 9 sequences of `--noEmit` runs and
emitting builds on 91 files (EMIT7 to EMIT9: body edits next to a file
that affects the global scope), the emitting builds write the same files
as with Go's walk (`GOPORT_DTS_WALK_FALLBACK=0`) in 43 of 45 cells, and
never fewer files in any cell. The other 2 have no declarations (with and
without `sourceMap`) and run a fresh `--noEmit` build, a `--noEmit` change
of `Shape` that skips the walk, an emitting build, and a `--noEmit` body
edit of b5.ts (it imports a.ts). The next emitting build then writes c5.js
(it imports b5.ts) again, and its map with `sourceMap`.

Go's tsc, tsbuild and tscWatch baselines never reach the limit: a
program has at most 23 such files, and at most 6 in a baseline that uses
`noEmit`. `GOPORT_DTS_WALK_FALLBACK=0` runs Go's walk and Go's
signatures. `scripts/goport/t3-incremental-bench.sh` measures it.
- When a changed file that affects the global scope gets a new d.ts
signature, every file makes its d.ts emit (Go
`handleDtsMayChangeOfAffectedFile`), one at a time, where Go runs them on
a work group (`collectAllAffectedFiles` loop 2). In T3 Code apps/web an
edit of a component with `declare global` takes about 2x a cold run, on
main and with the fallback (`t3-incremental-bench.sh -c web-global`, wall
medians of 5 runs: 2.40 -> 4.97 s on main, 2.42 -> 5.18 s with it).

`program.rs` defines `SourceFileInfo`, `load`, `bind_all`, the Go
`Program` methods as free functions with Go snake names (`get_resolved_module(file, name, mode)` ->
Expand Down Expand Up @@ -1229,6 +1326,16 @@ process (bin/tsgo.rs `unblock_go_signals`, `go_runtime_start`).
the wait for the check, less the nested declaration emit time, as in Go.
"Emit time" is that nested emit time plus the wait for the rest of the
emit.
- In `programToSnapshot` the referenced files job of each file also
compares the new set with the old one (`ReferencedFiles::same_as_old`;
t3inc2), as Go does in its per-file work group (programtosnapshot.go:111),
not on the loading thread. The job gives back the old set when it has the
same program files in another order
(`ReferencedFileSet::same_as`): a set read from build info is in file id
order, and no reader of a set of program files depends on its order (Go
iterates a set in random order). On T3 Code apps/web after a contracts
change (327,661 references), the loading thread's own work in the change
loop went from about 29 ms to about 16 ms.
- Transformers return factory (synthetic) SourceFiles. `source_file_info`
and the printer's identifier set map one to the parsed file with the same
path (Go `copyFrom`). `get_ecma_line_starts` caches its line map by node,
Expand Down
Loading
Loading