Repository navigation
v4: move to TypeScript 7, framework support via content mappers - #103
Open
johnsoncodehk wants to merge 54 commits into
Open
johnsoncodehk wants to merge 54 commits into
johnsoncodehk wants to merge 54 commits into
Conversation
- @tsslint/cli now depends on typescript-native-bridge (pinned 6.0.3-bridge.15.tsgo.7.0.2) instead of the typescript peer dep; type checking runs on the TS 7 (tsgo) engine - Remove --vue-project/--vue-vine-project/--mdx-project/--astro-project/ --ts-macro-project flags and the Volar language plugin wiring (@volar/language-core, @volar/language-hub, @volar/typescript) - Meta-framework support will be provided by the TS 7.1 content mapper (tsconfig contentMappers) once TNB rebases onto tsgo 7.1 - Remove fixtures/meta-frameworks and untracked tsgo/relay experiments
The file was deleted with the Volar wiring; the hardcoded corpus list in packages/compat-eslint/test/bench/dogfood-corpus.ts still referenced it, failing lazy-estree.test.js with ENOENT in CI.
…lumbing - packages/cli now pins typescript-native-bridge 6.0.3-bridge.16.tsgo.7.0.2 - remove the languages parameter from the cache layer (dead since the Volar removal); the cache key no longer carries it, which also invalidates existing caches
- README: real framework-support section (tsconfig contentMappers), updated requirements (bundled engine, no typescript pin, musl unsupported) - docs/migration-v4.md: breaking changes, flag -> contentMappers mapping, engine behavior notes, editor status, availability gates
TypeScript 7 has no tsserver plugin API, so @tsslint/typescript-plugin and the VS Code extension built on it cannot work.
- Build the workspace with typescript@7.1.0-beta (module nodenext,
Node >= 22.12 for require(esm)); drop typescript-native-bridge.
- @tsslint/types: SourceFile from typescript/ast, Program/Checker/
DiagnosticCategory from typescript/sync. RuleContext gains `checker`
(TS7 programs have no getTypeChecker); LinterContext is
{ typescript, getProgram }.
- @tsslint/core: lint() takes { program, checker, file } instead of a
file name resolved through a LanguageService.
- Add tools/estree-oracle, a private test-only package pinning
typescript@6 + typescript-eslint as the parity reference.
- Port core tests to real TS7 projects; drop the BuilderProgram POC.
tsl and tslint are built on the TypeScript 5/6 API and cannot run on TS7. The eslint, ignore, category and diagnostics plugins now use typescript/ast and typescript/sync.
- lazy-estree / tokens / ts-ast-scan read typescript/ast. TS7 shape differences are handled in lib/ts.ts helpers: postfixToken for ?/!, ModuleDeclaration.keyword, phaseModifier for type-only imports, heritage clauses parsed as TypeReference, synthesized (Reparsed) nodes, nameless array-binding holes, EndOfFile/JSDoc kinds. - Scope analysis uses @typescript-eslint/scope-manager's analyze(), built lazily on first scopeManager read, replacing TsScopeManager. - parserServices type queries go through the project checker. - Parity tests compare against typescript-estree running on its own TypeScript 6 copy (tools/estree-oracle), bridged by node ranges. Tests depending on @typescript-eslint/eslint-plugin or the vendored scope-manager suite are removed: typescript-eslint cannot load TS7. - dogfood: register all rules before the first file is linted, so the shared traversal sees every rule (previously only the first one ran).
- lint through typescript/sync snapshots; project file names come from the configured project so content-mapped files (e.g. .vue) are included - replace the BuilderProgram incremental state with a program fingerprint (cache format v3) - map diagnostics back to original files via SourceFile.spanMap; drop diagnostics that only cover generated code - apply --fix only when every edit of a fix maps exactly - render with the TS7 formatDiagnosticsWithColorAndContext
Owner
Author
|
/release alpha 4.0.0 |
Contributor
|
Alpha release failed, see https://github.com/johnsoncodehk/tsslint/actions/runs/39767990607 |
Owner
Author
|
/release alpha 4.0.0 |
Contributor
|
Alpha release failed, see https://github.com/johnsoncodehk/tsslint/actions/runs/39770287974 |
Owner
Author
|
/release alpha 4.0.0 |
Contributor
|
Alpha release failed, see https://github.com/johnsoncodehk/tsslint/actions/runs/39771220489 |
Owner
Author
|
/release alpha 4.0.0 |
Contributor
|
Published |
Record the version published from f7d6918 by release run 39772141598, so the next `prerelease` release is 4.0.0-alpha.1. Only workspace specifiers change in pnpm-lock.yaml.
- Every package that loads `typescript/sync` / `typescript/ast` now depends
on typescript 7.1.0-beta instead of peering on it. TSSLint's checker is
its own engine; as a peer, pnpm resolved it to a host project's TS 6 and
`require('typescript/sync')` failed.
- @tsslint/config and @tsslint/compat-eslint declare an optional eslint
peer (>=9.39.0 <10.0.0). importESLintRules() loads core rules from
`node_modules/eslint/lib/rules`, and both packages expose ESLint types.
- @tsslint/config peers on @tsslint/compat-eslint via `workspace:*`, so
the published range always matches the release (it was `^3.0.0`).
…kind scans
ParenthesizedExpression, ParenthesizedType and ComputedPropertyName are
pass-through in convertChildInner: materialise returns the inner ESTree.
The scan relied on the child's target equalling the parent's to skip the
re-entry, but when the inner is a wrapper head (`(a?.b)` ->
ChainExpression, `(typeof import('x'))` -> TSTypeQuery) the parens level
expands the chain and leaves its tail as parentTarget, so the child's
visit entered the same chain again. In CPA and wildcard mode every
listener on those nodes fired twice (three times under `((a?.b))`), e.g.
no-non-null-asserted-optional-chain reported `(a?.b)!` twice.
The three kinds now join the structural pass-through list and recurse
with the parent's target unchanged.
getSemanticDiagnostics on a content-mapped file already returns positions in the original file; format.ts mapped them through spanMap again as if they were transformed coordinates, which dropped or misplaced them. Only rule reports are in transformed coordinates.
Rule reports are TypeScript's Diagnostic plus a ruleId, and fixes and refactors return TextEdit[] for the reported file. TextSpan, TextChange, FileTextChanges, RelatedInformation and the custom Diagnostic shape are removed. Plugins receive the lint target instead of the file, which replaces LinterContext.getProgram. Compiler diagnostics are passed through as returned by TypeScript. The cache format is bumped to v4.
A crash was reported at offset 0 of the file, which pointed at an unrelated line and was dropped when offset 0 of a content-mapped file is generated code. It now has no fileName, so directives and ignore comments do not apply to it, and the CLI prints it with the file name only.
Replace the syntactic / type-aware rule split and the whole-program fingerprint with tsc-incremental-style invalidation. Each file's result after plugins ran is cached as a whole and reused while the file is unchanged and none of its dependencies changed their declaration signature. A changed signature re-lints direct importers and keeps propagating only while signatures keep changing; a declaration that names a changed file (e.g. a re-export) counts as changed. Compiler options, tsconfig files, content mappers, the config and every module it imports, the program's file list, files under node_modules, and edits to files that declare globals re-lint everything. Core no longer probes rules for program access, and --list-rules is removed. withoutCache() now keeps the reported file out of the cache.
Rules must only depend on the program. Inputs outside it are not tracked, and --force re-lints every file.
A rule can read another file's implementation, e.g. the body of a function it calls. Core records every other program file a rule reaches through a declaration node, a source file, diagnostics, emit, or a checker query by file, and the CLI keeps the file's result only while those files keep the same text.
Names inside `declare module './a' { ... }` resolve in './a', so the declaration emit can drop the import that names them. A signature change of './a' then changes the augmentation without changing its text.
… detection gaps - treat a UMD module (export as namespace) as global - depend on modules a file augments without importing them - read import types with attributes and type references with attributes before types as references - re-lint everything only when a declare global / declare module block changes, or a changed signature reaches what a block names; other changes of the module re-lint its importers
…dules The program emits no declarations for a workspace package or a referenced project's source behind a symlink, which fell back to a text hash as its signature. Emit them as root files of a program with the same options and project references.
… reaches Per export and top-level declaration, store the hash of its declaration emit and the names the checker resolves its source identifiers to, across files. A name changed when a hash it reaches changed, so same-named properties no longer pull in other declarations and a change two modules away only re-lints the names that reach it. Cache format v9.
…ach them Names are stored with their hashes, and what each declaration references is resolved through the checker the first time a change reaches its module, a dep or a module a dep re-exports. The graph also only expands those modules, so a body edit with unchanged hashes walks no names. Cache format v10.
The cache keeps a hash per declaration, export and import; a base64url prefix of the SHA-256 digest is a quarter of the hex digest, which makes the cache file a quarter smaller to load and save. Cache format v11.
Module.registerHooks, which tracks the modules a config imports, was added in Node.js 22.15. On 22.12 to 22.14 the cache ignored edits to rule files and other config imports. Also correct the README on which importers an exported type change re-lints.
Upgrading a mapper's dependencies (e.g. @vue/language-core) changed neither its package.json nor its exec files, so mapped files kept stale results. Hash the nearest lockfile above each mapper package.
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.
Summary
TSSLint v4 runs only on TypeScript 7.1. Rules get the TypeScript 7 AST (
typescript/ast), plus theProgramandCheckerfromtypescript/sync. There is no TypeScript 5/6 code path and no compatibility layer. Framework files (.vue,.astro,.mdx, …) are linted through TypeScript 7.1's Content Mapper (tsconfigcontentMappers) instead of Volar language plugins, so the tsconfig is the single source of truth. The CLI cache now decides what to re-lint from a dependency graph, the waytsc --incrementaldoes, instead of splitting rules into syntactic and type-aware.Published so far as
4.0.0-alpha.1(npm dist-tagalpha). This branch is 38 commits ahead of that tag, including the result API change and the new cache.Quick start
npm install @tsslint/cli@alpha @tsslint/config@alpha --save-dev # TypeScript 7.1 comes with them npx tsslint --project tsconfig.jsonBreaking changes
See
docs/migration-v4.mdfor the full guide.typescript7.1.0-beta(exact), separate from thetypescriptyour project compiles with; thetypescriptpeer dependency is gone. Diagnostics come from the TypeScript 7 checker and can differ from TypeScript 5/6 in edge cases.Module.registerHooks, added in 22.15.typescriptis now thetypescript/astmodule;programis atypescript/syncProgram;checkeris passed directly;ts.forEachChild(node, cb)becomesnode.forEachChild(cb);ts.sysand friends are gone (usenode:fs/node:path).typescript/syncDiagnostic(pos,end,text) plusruleId.withFix/withRefactorreturnTextEdit[]({ pos, end, newText }). PluginresolveDiagnostics/resolveCodeFixesreceive the lint target ({ program, checker, file }) instead of the file.@tsslint/typescript-pluginand the VS Code extension are removed. TypeScript 7 has no tsserver plugin API, so v4 has no editor integration.importTSLintRulesandfromTSLRulesare removed. Both depend on the classictypescriptAPI.--vue-project,--vue-vine-project,--mdx-project,--astro-projectand--ts-macro-projectare removed, together with the Volar dependencies. DeclarecontentMappersin the tsconfig instead; there is no replacement flag.--list-rulesandReporter.withoutCache()are removed. There is no syntactic / type-aware split left to list. Rules must only depend on the program; reads throughfs, environment variables and the like are not tracked, and--forcere-lints every file.v11(v3 shippedv2), so the first v4 run re-lints everything.@typescript-eslint/eslint-plugindoes not load withtypescript@7: its dependencies read the classictypescriptAPI at module load.importESLintRulesstill works for ESLint core rules and plugins that don't importtypescript.@tsslint/coreAPI (only for embedders):createLintertakesctx: { typescript: typeof import('typescript/ast') },linter.linttakes{ program, checker, file }instead of a file name,applyTextEditsworks on strings, andlinter.getTouchedFiles(fileName)is new.Remaining work
typescriptdependency from7.1.0-betato the stable 7.1 release once it is published.Done
Engine
@tsslint/types,@tsslint/core,@tsslint/configand@tsslint/cliare ported to the TypeScript 7.1 API (typescript/ast,typescript/sync).--fixupdates the snapshot after each write before the next pass.@tsslint/compat-eslint's lazy ESTree is ported to the TypeScript 7 AST.typescriptis a direct dependency of every package; ESLint is declared as a peer (>=9.39.0 <10.0.0) where needed.Content Mapper support
runExternalCode, so mappers declared in the tsconfig run as part of loading the project; there is no flag to pass.file.text) and report in its coordinates. Diagnostics are mapped back to the original file through the mapper's span map; those that only cover generated code are dropped.--fixapplies a fix only when every edit maps exactly onto original text, and applies an edit once when a mapper copies the same code more than once.@vue-ignore,@vue-expect-error) are suppressed, and compiler diagnostics of mapped files are not mapped twice.Volar removal
packages/cli/lib/languagePlugins.ts,fixtures/meta-frameworksand thelanguagesplumbing in the cache are removed.packages/typescript-pluginandpackages/vscodeare removed.Cache
Stored at
os.tmpdir()/tsslint-cache/<tsslint version>/<typescript version>/<hash of tsconfig and config path>.cache.json, so a new TSSLint or TypeScript version starts cold. Formatv11; any parse, shape or version mismatch is treated as a cold start, and writes go through a temp file and rename.tsslint.config.tsor a module it imports, the set of program files, or anything undernode_moduleschanges; also when the declaration signature of a script or UMD module changes, or what adeclare global/declare module '...'block declares.A.vuedoes not re-lintB.vuethat imports it.Docs
docs/migration-v4.md: breaking changes, rule context mapping, flag →contentMappers, caching, ESLint interop,@tsslint/coreAPI, framework projects.typescriptinstall, caching, framework support, requirements.CI and release
alphadist-tag (4.0.0-alpha.0,4.0.0-alpha.1).Tests
All CI steps pass at
2290a30(PR CI green; also run locally on Node 24.15.0,typescript7.1.0-beta):.vue) and render tests.pnpm run lint --forceon the repo itself: 53 passed.Notes
typescriptyour project uses; anything they need from TypeScript comes fromtypescript/astand theProgram/Checkerthey are given.--force.tsslint.config.tsis loaded with--experimental-strip-types, unchanged from v3.References
docs/migration-v4.mdREADME.md#caching,packages/cli/lib/invalidation.ts