Isolated reproduction for typescript-eslint issue #12708.
TypeScript's ProjectService adds up the size of every program root it does not
consider TypeScript. Past maxProgramSizeForNonTsFiles — 20MB, not configurable —
it disables the project's language service, and a disabled project reports only the
files the client currently has open as program roots. parserOptions.projectService
opens one file at a time, so past that threshold every linted file gets its own
ts.Program, and files the linted file does not import are no longer in it.
Nothing reports that this happened.
npm installgenerate.mjs writes the two generated parts of the fixture: src/bulk/*.js,
filler whose only job is to add up to a given number of megabytes, and
src/app/*.ts, 100 small files for measure.mjs to parse one at a time.
src/ambient.d.ts declares a global. Nothing imports it — it is in the program
because the tsconfig includes it, which is why a demoted project loses it.
src/target.ts uses that global.
node generate.mjs 5 # 5MB of .js, under the limit
npx eslint src/target.tsClean.
node generate.mjs 25 # 25MB of .js, over the limit
npx eslint src/target.tssrc/target.ts
1:14 error Unsafe assignment of an error typed value @typescript-eslint/no-unsafe-assignment
2:14 error Unsafe assignment of an error typed value @typescript-eslint/no-unsafe-assignment
2:24 error Unsafe call of a type that could not be resolved @typescript-eslint/no-unsafe-call
2:37 error Unsafe member access .toUpperCase on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
Same file, same rules, same parser. The only difference is how many bytes of
unrelated JavaScript sit next to it. Uncommenting "disableSizeLimit": true in
tsconfig.json makes it clean again.
measure.mjs parses each src/app/*.ts the way ESLint would, one at a time, and
counts distinct ts.Program instances.
| fixture | distinct programs | program source files | ms/file |
|---|---|---|---|
node generate.mjs 5 |
1 | 196 → 196 | 3.2 |
node generate.mjs 25 |
100 | 90 → 189 | 6.2 |
node generate.mjs 25 + disableSizeLimit |
1 | 216 → 216 | 4.0 |
One program per file, growing as it goes. On a real app — 12,735 files — the same pattern cost 358 new programs per 400 files and about 4x per-file parse time.
| package | version |
|---|---|
@typescript-eslint/parser |
8.67.0 |
@typescript-eslint/eslint-plugin |
8.67.0 |
typescript |
5.9.3 |
eslint |
10.8.1 |
This reproduction was built by Claude (Anthropic's Claude Code) while investigating a lint slowdown in a large Ember app, and is published from Peter Wagenet's account.