Stop .gts counting against TypeScript's non-TS program size limit - #247
Open
wagenet wants to merge 1 commit into
Open
Stop .gts counting against TypeScript's non-TS program size limit#247wagenet wants to merge 1 commit into
wagenet wants to merge 1 commit into
Conversation
TypeScript's ProjectService sums the size of every program root it does not recognise as TypeScript and, past maxProgramSizeForNonTsFiles (20MB), calls disableLanguageService on the project. A disabled project reports only the files the client currently has open as program roots. parserOptions.projectService opens one file at a time, so against a demoted project every linted file forces a fresh ts.Program. That is several times slower, and it also changes results: ambient declarations the linted file does not import (declare global, module augmentation, standalone .d.ts) are no longer in the program, so type-aware rules see `any` where they would otherwise see a real type. .gts is TypeScript with a template in it and has no business being weighed against a JavaScript budget. There is no way to say so through extraFileExtensions — an extension registered as ScriptKind.TS is dropped from the supported set outright — so report .gts as weightless through the patched ts.sys instead. .gjs is JavaScript and keeps its weight. That patch only lands when this parser replaces ts.sys before typescript-eslint builds its ProjectService, and an app can be over the limit on .js + .gjs alone, so also surface the demotion: hook the public Project#disableLanguageService and warn once, naming the project and disableSizeLimit. Installed at import because the project service is built on the first type-aware parse in the process, which may be a plain .ts file this parser never sees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 15, 2026
Contributor
🏎️ Benchmark ComparisonParse
Full mitata output
Full mitata output |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
TypeScript's
ProjectServiceadds up the size of every program root it doesn't recognise as TypeScript. PastmaxProgramSizeForNonTsFiles— 20MB, not configurable — it callsdisableLanguageServiceon the project. A disabled project reports only the files the client currently has open as program roots:parserOptions.projectServiceopens one file at a time. So against a demoted project, every file ESLint lints is new to the program and forces a freshts.Program..gtsis TypeScript with a template in it, and it counts toward that budget. A large enough TypeScript-flavoured Ember app trips a limit meant for apps full of bundled JavaScript, and nothing says so.Classic
parserOptions.projectis unaffected — typescript-estree builds its own program and never consults the heuristic. It only bites the mode typescript-eslint now recommends, which is probably why it's gone unnoticed.It's not only slower — results change
This is the part that made me write it up as a bug rather than a perf tweak. A demoted program contains the open file and whatever it imports, and nothing else. Ambient declarations that nothing imports —
declare global, module augmentation, standalone.d.ts— simply aren't there.Same file, same rules, two projects identical except for total
.gtsbytes:AMBIENT_FLAGstringanywindow.ambientRegistryAmbientRegistryanyCannot find name 'AMBIENT_FLAG',Property 'ambientRegistry' does not exist on type 'Window & typeof globalThis'Every
no-unsafe-*rule,no-floating-promises,restrict-template-expressions— anything that branches onany— reports differently once a project crosses the line. Ember apps lean on registry augmentation, so this isn't hypothetical.Numbers
Synthetic fixture, 22MB of
.gts(313 files) over a 200-module import chain, 300 files linted, measuringparseForESLintonly:ts.PrograminstanceslanguageServiceEnabledfalsetrueThe rising per-file cost is the mechanism: each rebuild is over a slightly larger program, so the gap widens with project size. At 12k files it's the difference between one program build and twelve thousand.
What this changes
1.
.gtsno longer counts toward the non-TS budget. The honest fix is to tell TypeScript that.gtsis TypeScript, but there's currently no way to say that — see below. So the patchedts.sysreports.gtsas zero bytes and the project gets measured on the JavaScript it actually contains..gjsdeliberately keeps its weight. It's JavaScript with a template; charging it to a JavaScript size budget is correct, not a bug.2. A one-time warning when a project does get demoted, naming the project, the file that tipped it over, and
disableSizeLimit. This hooks the publicProject#disableLanguageService, calls straight through, and changes nothing.What this does not fix
An app can be over the limit on
.js+.gjsalone, and excluding.gtswon't save it. Real numbers from the app where this turned up:That app stays demoted, which is why change 2 matters as much as change 1: it turns an invisible 4x into a line of output that names the fix.
"disableSizeLimit": truein the tsconfig resolves it completely — on the app above it took rebuilds from 358-per-400-files to 1, with slightly lower peak memory.Worth considering separately: the Ember app blueprint could ship
disableSizeLimit: true, since any large app using template imports will eventually trip a limit designed for a different situation. That's a blueprint/docs change elsewhere, not this repo.Known limitation
Change 1 only lands if this parser replaces
ts.sysbefore typescript-eslint builds itsProjectService. That service snapshots{...ts.sys}once, on the first type-aware parse in the process — and if ESLint reaches a plain.tsfile first, that parse happens before this parser is ever invoked, and the patch misses. I measured both orderings:.gts.tsChange 2 is installed at import for exactly this reason, so the warning is reliable even when the workaround isn't. Same caveat already applies to the existing
readFile/fileExists/readDirectorypatches — I've left that alone here rather than widen this PR.Upstream
The real fix belongs in TypeScript, and
extraFileExtensionslooks like the place to declare that.gtsis TypeScript. It isn't, yet —getSupportedExtensionsonly accepts extra extensions registered asDeferredor JS-like, and silently drops anything registered asScriptKind.TS:.gtsregistered asDeferred(what typescript-eslint does today)TS.gtsdropped from the project entirelyTSTwo small hunks make row 3 work: let
getSupportedExtensionsaccept TS-like extra extensions, and have the size check skip them. I've tested that patch against a local TypeScript build; issues filed:The typescript-eslint side is where the warning really belongs, too. TypeScript already emits a public
projectLanguageServiceStateevent carrying the project andlastFileExceededProgramSize, and typescript-eslint already builds aneventHandler— it just wires it to a debug namespace that's off by default. Surfacing it there needs no new API from anyone, and would make the hook in this PR redundant.Testing
tests/size-limit.test.js— unit cover for the patchedgetFileSizeand the warning hooktests/size-limit-project.test.js— end-to-end: builds two 21MB projects in tmp, one carrying its bytes in.gtsand one in.gjs, and asserts the first keeps a whole program while the second demotes and warns. Runs in ~1.4s, and fails onmain.Feature-detects
projectServiceand skips where it's unavailable. Verified green against@typescript-eslint/parser6/7/8 and TypeScript 5.3 / 5.7 / 5.9, pluspnpm --filter '*' testandtest:check.Needs a label for release-plan —
bugseems right.