Skip to content

Support TypeScript 7.1 API - #1704

Merged
johnnyreilly merged 165 commits into
nextfrom
copilot/implement-new-tsgo-api-support
Oct 22, 2026
Merged

johnnyreilly merged 165 commits into
nextfrom
copilot/implement-new-tsgo-api-support

Conversation

Copilot AI commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Copilot didn't write this - I did! The below can also be found in the CHANGELOG.md.

This is a ground-up rewrite of ts-loader's compilation engine. Instead of driving TypeScript's classic LanguageService / Program / watch APIs, ts-loader now compiles exclusively through TypeScript's new native typescript/sync API (the tsgo-powered engine) - the legacy compiler API integration has been removed entirely.

Not supported yet

  • getCustomTransformers, resolveModuleName and resolveTypeReferenceDirective are still accepted for backwards compatibility but are now inert - the native API doesn't expose equivalent extension points, so custom transformers and custom module/type-reference resolution are no longer applied. It is possible that the API will support these in future, and so the options have been left in place for now, but they will be removed if the API never exposes them.
  • Project references in tsconfig.json are followed by the native API. The projectReferences loader option is accepted but has no effect, and v10 doesn't build referenced projects to disk the way tsc --build does.

Breaking changes:

  • Minimum supported TypeScript version is now 7.1+ (up from 3.6.3+). Until TypeScript 7.1 has a stable release, that means the 7.1 beta (typescript@beta) or later.
  • Minimum supported Node.js version is now 22.x+ (up from 12.x+).
  • The compiler option must now resolve to a package exposing a <compiler>/sync entry point (the TypeScript native API). Drop-in classic-API compilers (e.g. ttypescript) are no longer supported.
  • Removed the compilerOptions loader option; the native API resolves a project's compiler options purely from its on-disk tsconfig.json, with no per-loader-instance override hook. Set compiler options in tsconfig.json instead.
  • Removed the context loader option; the native API always resolves relative paths against the config file's own directory, with no basePath override exposed to let a tsconfig live outside the project root.
  • Removed the happyPackMode loader option - HappyPack / thread-loader based parallelisation is no longer supported this way.
  • Removed the experimentalFileCaching loader option; there's no equivalent under the native API's own caching model.
  • Removed the experimentalWatchApi loader option, now that the native API supersedes TypeScript's classic watch API.
  • Removed the onlyCompileBundledFiles loader option; the native API always resolves a project's own root files and offers no hook to restrict them to what webpack actually bundles.
  • Info-level log messages are now written to stderr by default, as documented; previously they always went to stdout regardless of logInfoToStdOut. Set logInfoToStdOut: true to keep them on stdout.
  • errorFormatter's colors argument is now a small picocolors-backed helper object instead of a chalk instance; chalk has been dropped as a runtime dependency in favour of picocolors.

@johnnyreilly

Copy link
Copy Markdown
Member

@copilot update

ts: [5.6.2, 5.7.2, 5.8.2, 5.9.2, 6.0.2] # next excluded for now
and
ts: [5.6.2, 5.7.2, 5.8.2, 5.9.2, 6.0.2] # next excluded for now
to only cater for: 7.1.0-dev.20260725.1 - the existing versions specified do not contain the new API.

We expect execution-tests to pass after this change

Copilot AI commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update

ts: [5.6.2, 5.7.2, 5.8.2, 5.9.2, 6.0.2] # next excluded for now
and https://github.com/TypeStrong/ts-loader/blob/53082fbe1cdeb560b6...

Updated in fcc8a21 to run the execution-test matrix only against 7.1.0-dev.20260725.1. I also verified the native path locally with yarn build && yarn execution-tests -- --single-test nativeApiTranspile.

@johnnyreilly

Copy link
Copy Markdown
Member

@copilot great work! We would like the following to succeed:

yarn build && yarn execution-tests

Remember backwards compatibility is not required - feel free to make changes

Copilot AI commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot great work! We would like the following to succeed:

yarn build && yarn execution-tests
...

Verified on fcc8a21: yarn build && yarn execution-tests now succeeds locally (30 execution suites passed).

@johnnyreilly

Copy link
Copy Markdown
Member

@copilot we have explictly removed the historic typescript version from dependencies and moved to use 7.1 directly. This has broken compilation. Please fix it. You will likely need to use new APIs in TypeScript 7 to achieve this. No cheating.

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot we have explictly removed the historic typescript version from dependencies and moved to use 7.1 directly. This has broken compilation. Please fix it. You will likely need to use new APIs i...

Updated in 4706cfe. yarn build now passes against direct TypeScript 7 by routing the loader through typescript/unstable/sync instead of the removed legacy compiler API.

@johnnyreilly

Copy link
Copy Markdown
Member

@copilot make experimentalNativeApi: true the default behaviour and ensure execution-tests pass. We are happy to ignore loaderOptions test for now

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot make experimentalNativeApi: true the default behaviour and ensure execution-tests pass. We are happy to ignore loaderOptions test for now

Addressed in 048c5ce. The native execution test now relies on the default native API path, loaderOptions is skipped for now, and yarn build && yarn execution-tests passes locally.

johnnyreilly and others added 9 commits October 20, 2026 20:08
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fs callbacks

Each compiled file's content is now added as a persistent fileSystem layer
on the instance snapshot (sent in the same request as any openFiles for it),
replacing the readFile/fileExists callbacks and runWithTemporaryFileUpdate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- install typescript@beta rather than next
- list projectReferences among the accepted-but-inert options
- correct errorFormatter's colors type

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useCaseSensitiveFileNames now defaults to probing the volume the tsconfig.json
is on (as tsc does, and as v9 did via ts.sys) instead of assuming
case-sensitive on every non-Windows platform, and the resolved value is passed
to the TypeScript API so both agree on which paths are the same file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnnyreilly
johnnyreilly force-pushed the copilot/implement-new-tsgo-api-support branch from d8b541b to 141bc0f Compare October 21, 2026 14:41
johnnyreilly and others added 5 commits October 21, 2026 15:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed assertion thrown from webpack's watch callback left the watcher
open, so mocha never exited; the harness now closes it and fails the test.
A patch file named <file>.__deleted__ now deletes <file> when applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- compare paths canonically (via resolvedFilePathCache) when finding a
  changed file's dependants: the API echoes back the casing it was given,
  so on a case-insensitive filesystem exact-string comparison found none
- treat a modified file the program didn't previously contain as created:
  clear directImportsCache (other files' imports may now resolve) and tell
  the API it was created
- merge, rather than overwrite, unconsumed watcher changes, so a rebuild
  that compiles nothing (e.g. a pure deletion) doesn't lose them

Adds the deletedFileWatch comparison test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ts-loader passed its canonical (lowercased, when case-insensitive) paths to
the API, which echoes back whatever casing it is given - so on Windows, and on
macOS since case sensitivity is detected, TypeScript's messages showed
lowercased paths. The canonical form is now only used for ts-loader's own
cache keys; openProjects, openFiles and file notifications get real-cased
paths.

The comparison test harness no longer forces useCaseSensitiveFileNames: true,
so the default is tested on every OS; snapshots change only in the
options-derived instance hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sending removePrograms in the same snapshot update as createPrograms crashes
tsgo 7.1.0-beta (nil dereference in ProjectCollectionBuilder.HandleAPIRequest),
so any project with more than 20 orphan files killed the compiler process
once the synthetic-program cap started evicting. Adds manyOrphanFiles (25
orphan files) as a regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
johnnyreilly and others added 2 commits October 21, 2026 20:01
Files outside the configured project each got their own synthetic program
(capped at 20, least-recently-used evicted), each a near-complete copy of
the project - so a tsconfig whose files list only a .d.ts (every module an
orphan) rebuilt programs constantly: 8.1s vs 0.6s for the same 300-file
fixture with an include glob. They now share one program, as classic
ts-loader's single language service did, whose extra roots are only the
orphan files it doesn't already contain via an import.

Being a superset of the primary project, it also gives the transitive-
dependant recheck the full import graph, so dependants of a changed orphan
file are rechecked again (a regression from classic).

Adds an all-orphan-files variant of the benchmark fixture with cold and
hub-touch typeCheck scenarios: -92% and -95% vs the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every module is an orphan (the tsconfig lists only a .d.ts); changing an
imported file's type must still be reported in the unchanged importer,
which only the transitive-dependant recheck catches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnnyreilly
johnnyreilly marked this pull request as ready for review October 21, 2026 19:07
@johnnyreilly
johnnyreilly changed the base branch from main to next October 22, 2026 04:59
johnnyreilly and others added 4 commits October 22, 2026 06:16
Clarified the status of unsupported features in the CHANGELOG.
Removed sections on upgrading to v10 and Yarn Plug’n’Play from the README.
- set version to 10.0.0-beta.0
- restrict typescript peer dependency to >=7.1.0-beta
- add a files allowlist so only built output is published
- document installing the beta via the next dist-tag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@acutmore

Copy link
Copy Markdown
Contributor

Here's what Codex had to say (feel free to ignore):

  • src/typeScriptApi.ts:156: diagnostics are requested only for webpack-compiled files. An unbundled file included by tsconfig can contain a type error while webpack succeeds. This unintentionally recreates onlyCompileBundledFiles.
  • src/diagnostics.ts:53: diagnostics are never evicted. I reproduced a watch build where an erroneous module was deleted and its import removed, but the next build retained its old TS2322 error. Declaration state has the same append-only risk.
  • src/typeScriptApi.ts:1306: dependency discovery ignores non-relative imports and manually approximates TypeScript resolution. I reproduced stale diagnostics through a type-only paths alias. The native Program.getResolvedModuleFromModuleSpecifier API should be the source of truth.

My architectural recommendation is to let the native Program own more semantic truth: whole-program diagnostics and resolved-module information, while ts-loader remains the webpack/snapshot adapter. That should simplify the large custom dependency/recheck machinery and make future project-reference support easier.

johnnyreilly and others added 2 commits October 22, 2026 11:17
- type-check project files webpack doesn't bundle (all on the first build,
  then the ones webpack's watcher reports changed), and watch them so edits
  trigger a rebuild - matching v9 without onlyCompileBundledFiles
- drop stored diagnostics and declaration files for files that are no
  longer in the program or a webpack module, e.g. deleted files
- comparison-test harness records diagnostics-only rebuilds (same hash)
- update fixture .d.ts files to valid TS 7 syntax

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dependency tracking and the transitive-dependant recheck only followed
relative specifiers, resolved by hand, so a change reached through a
tsconfig paths alias (or baseUrl, package exports, ...) left stale
diagnostics. Resolve every import via the program's
getResolvedModuleFromModuleSpecifier instead, batched into one round trip
per pass, skipping unresolved and external-library imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnnyreilly

Copy link
Copy Markdown
Member

Thanks @acutmore - all three were real, and reproduced. Fixed in:

  • Unbundled files' diagnostics - 6d52b07. The end-of-build pass now type-checks every program file webpack doesn't bundle (all of them on the first build, or whenever webpack can't say what changed; afterwards just the ones its watcher reports changed), and adds them as file dependencies so editing one triggers a rebuild. That restores v9's default (no more accidental onlyCompileBundledFiles) and goes slightly further, since v9 never watched those files. Test: unbundledFileErrorsWatch.
  • Diagnostics never evicted - also 6d52b07. Stored diagnostics and declaration files are dropped for any file that's no longer in the program and no longer a webpack module, so a deleted file's error goes away. Test: deletedFileErrorsWatch.
  • Non-relative imports - 98b1d22. The hand-rolled relative resolver is gone; dependency tracking and the dependant recheck now use Program.getResolvedModuleFromModuleSpecifier, so paths/baseUrl/exports all count (unresolved and external-library imports are skipped). Since that's one request per specifier, they're sent via api.batch - benchmark is flat to ~10% faster on hub-touch rebuilds. Test: pathsAliasWatch (type-only import through a paths alias).

Turning on the unbundled check also surfaced real TS 7 errors (TS1540 declare module X {) in a few fixtures' own .d.ts files, which I updated to valid syntax.

Agree with the architectural direction - leaning on the native Program for resolution is a step that way, and it'd be good to keep shrinking the custom dependant/recheck machinery as the API grows.

@johnnyreilly

Copy link
Copy Markdown
Member

okay I think we're ready to ship a beta - I'll merge this into a next branch and start that!

@johnnyreilly
johnnyreilly merged commit 4244ab5 into next Oct 22, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants