Repository navigation
Description
Feature category
Tooling Integration / Developer Experience / Performance Enhancement
Summary
Revised plan for native TypeScript 7 support of .tsrx files through the upstream content-mapper protocol. This supersedes #38. The direction in #38 is right; several of its premises were checked against live sources on 1 September 2026 and are corrected here. The corrections make the scope smaller, not larger.
Goal unchanged: integrate TSRX with native TypeScript (tsgo) via content mappers, reusing the existing target-specific type-only transform, for React, Preact, Solid, Vue, external Ripple, and explicitly selected third-party compilers. Ship an opt-in CLI path first, then editor integration. Keep the classic Volar path working until the native path has demonstrated parity for its advertised scope.
Corrections to #38 (verified 1 September 2026)
1. Content mappers are in the published nightly today
#38 says no published package contains the feature. That was checked against [email protected] and @typescript/[email protected], and is stale. Installed [email protected] and ran the bundled native binary against a tsconfig declaring a mapper:
| Invocation | Result |
|---|---|
tsc -p . with contentMappers in tsconfig |
TS100024: Content mappers require the '--runExternalCode' command line flag to be enabled. |
tsc -p . --runExternalCode |
TS100031: The content mapper package '@tsrx/content-mapper' could not be resolved. |
tsc -p . --runBogus (control) |
TS5023: Unknown compiler option '--runBogus'. |
The nightly parses contentMappers, honors the flag, and resolves the mapper package. No source build of typescript-go is needed. Pin an exact nightly as a devDependency. The July @typescript/native-preview pin in the root package.json does not contain the feature and must not be confused with it.
2. The native semantic API is not a prerequisite for editor integration
#38 assumes editor work needs the unstable/* API connection tracked in microsoft/TypeScript#63800. None of TSRX's language-server plugins call TypeScript's language service or checker. They wrap the results Volar's TS service already returned and post-process them using mapping metadata:
| Plugin | What it does today | Native equivalent |
|---|---|---|
hoverPlugin.js |
deobfuscates _$_ identifiers in TS hover text; injects CSS-class hover from customData.hover |
Alias span kind for diagnostics; TSRX server serves its own hover for CSS-class spans (VS Code merges hover providers) |
definitionPlugin.js |
redirects CSS-class spans into the <style> region via customData.definition |
TSRX server serves its own definition provider for those spans |
documentHighlightPlugin.js |
adds keyword highlights from customData.wordHighlight when TS returns none |
TSRX server document-highlight provider (see open question on provider merging below) |
typescriptDiagnosticPlugin.js |
filters codes from customData.suppressedDiagnostics; deobfuscates messages; suppresses TS diagnostics while fatalErrors is non-empty |
see correction 3; Alias; diagnostics field of the transform result |
completionPlugin.js |
adds TSRX snippets and Ripple-gated snippets; no TS calls | unchanged, served by the TSRX server |
autoInsertPlugin.js, documentSymbolPlugin.js, compileErrorDiagnosticPlugin.js |
no TS calls | unchanged |
typescriptService.js |
patches getUserPreferences to default preferTypeOnlyAutoImports |
becomes a documented user setting; no mapper equivalent |
Nothing above needs a Program or TypeChecker. The only capability with no native equivalent is modifying TypeScript's own responses (identifier deobfuscation in hover text). That is roadmap item 1B "LSP client middleware registration" in microsoft/TypeScript#63875, which is not built. Its absence is cosmetic for TSRX.
3. There is no per-code diagnostic filtering to migrate
#38 lists "diagnostic compatibility" as a required workstream, mirroring Vue's largest cost. TSRX's plumbing for suppressedDiagnostics exists in packages/tsrx/src/transform/segments.js (line ~2420) and in the language-server filter, but no transform in any target package emits that metadata. The workstream reduces to: delete the dead plumbing, and keep the check that no new suppression is introduced. The fatalErrors behavior (suppress all TS diagnostics while the file fails to compile) is a separate design decision, covered under "Compile-failure fallback" below.
4. Volar is not being adapted upstream, and does not need to be
The Vue migration (vuejs/language-tools#6170) lives in Vue's repo, not Volar's, because the content-mapper protocol replaces Volar's TypeScript host layer (@volar/typescript: runTsc, proxied language-service host, tsserver plugin). TypeScript 7 does not load tsserver plugins, so there is nothing left for that layer to patch. What survives is the @volar/language-core data model, which Vue keeps as a dependency for mapping structures. The volar.js repository has no content-mapper work and no substantive commits since March 2026.
TSRX may keep @volar/language-core as a mapping data structure if convenient. Everything under @volar/typescript, @volar/language-server, and volar-service-typescript is on the retirement path.
5. Non-VS Code editors get native support through the TS 7 LSP itself
For configured projects, the native LSP reads contentMappers from tsconfig, spawns the mapper, and registers document filters for the mapped extensions with the client. Any LSP client running TypeScript 7's language server gets .tsrx type intelligence without a per-editor integration, subject to the client sending initializationOptions.runExternalCode: true (the trust gate). Only inferred projects (files with no tsconfig) need the VS Code-specific registerContentMappers API. Zed, Neovim, IntelliJ, and Sublime documentation therefore becomes "point at the TS 7 LSP with this option", not a fresh integration each.
6. Declaration naming
Native emit names a content-mapped file's declaration Component.d.tsrx.ts and keeps ./Component.tsrx in specifiers. microsoft/TypeScript#64120 (open) adds outputExtension so a build that compiles .tsrx to .js can emit Component.d.ts. Track it; do not design around the current naming.
Protocol reference
Verified against tsc/internal/contentmapper/ and tsc/internal/spanmap/ at microsoft/TypeScript main, and packages/content-mapper/protocol.ts on vuejs/language-tools#6170.
tsconfig declaration
{
"contentMappers": [
{ "package": "@tsrx/content-mapper", "extensions": [".tsrx"], "options": { /* mapper-defined */ } }
]
}The package is resolved by node resolution from the tsconfig directory and is never executed at resolve time.
Mapper package manifest (package.json)
{
"name": "@tsrx/content-mapper",
"version": "x.y.z",
"typescript": {
"contentMapper": {
"exec": ["node", "dist/server.js"],
"compilerOptions": ["jsx", "jsxImportSource"],
"dynamicConfig": true
}
}
}name + version form the mapper identity used by incremental builds. compilerOptions lists the options that affect transform output. dynamicConfig: true enables the configIdentity/watchedFiles contract.
Transport: stdio, Content-Length framed JSON-RPC 2.0. TypeScript is the only requester; mappers never send requests or notifications. initialize has a 5 second timeout. Everything a mapper logs must go to stderr. TS_CONTENT_MAPPER_DEBUG logs the JSON-RPC traffic.
Methods
| Method | Params | Result |
|---|---|---|
initialize |
{ protocolVersion: 1, locale?, positionEncodings: ('utf-8'|'utf-16')[] } |
{ protocolVersion, positionEncoding, diagnosticSource } |
openProject |
{ configFileName, projectHandle, options?, compilerOptions } |
{ configIdentity, watchedFiles?, optionDiagnostics? } |
transform |
{ fileName, content, projectHandle?, options?, compilerOptions? } |
{ text, extension, mappings?, diagnostics?, diagnosticDirectives?, supplemental? } |
closeProject |
{ projectHandle } |
none |
Span mappings: [generatedStart, generatedLength, originalStart, originalLength, kind, features].
kind:Verbatim(0, same length and identical text),Atom(1, same entity, different text or length),Alias(2, different text; diagnostic text shows the original when the diagnostic covers the whole alias span exactly).features: bitmask over Hover, SignatureHelp, Completion, Definition, TypeDefinition, Implementation, SourceDefinition, References, DocumentHighlights, Rename, CallHierarchy, CodeActions, Formatting, InlayHints, SemanticTokens, FoldingRanges, SelectionRanges, LinkedEditing, AutoInsert, DocumentSymbols, CodeLens.- Validation: segments ordered and disjoint in virtual space, within both texts, verbatim text equal. Gaps are synthesized content. Diagnostics in gaps are reported, not dropped. Diagnostics ignore feature bits. Text edits (rename, code actions, completions, formatting) write back only through exact Verbatim geometry.
Other transform fields
diagnostics: syntax errors in the original file,{ start, length, code, messageText }in original offsets. This is where TSRX compile errors go.diagnosticDirectives:[originalStart, originalLength, virtualStart, virtualEnd, policy (0 Ignore | 1 Expect), unusedExpectDirectiveIndex?]plusunusedExpectDirectiveDiagnostics.supplemental: extra virtual files, each{ text, extension, mappings, diagnosticDirectives? }, named<file>.<index><ext>by the compiler..mts/.cts/.mjs/.cjsextensions force module scope.extension: one of.js .jsx .mjs .cjs .ts .tsx .mts .cts .json.
Reference implementations
- feat: content mapper (v4 alpha) vuejs/language-tools#6170,
packages/content-mapper/(worker pool, diagnostic directives, span conversion from Volar mappings) - https://github.com/NullVoxPopuli/ember-content-mapper
- https://github.com/remcohaszing/mdx-content-mapper (includes a minimal VS Code extension calling
registerContentMappers)
Mapping TSRX's current virtual code onto the protocol
Sources: packages/typescript-plugin/src/language.js, packages/tsrx/src/source-map-utils.js, packages/tsrx/src/transform/segments.js.
| Current concept | Native equivalent | Notes |
|---|---|---|
TSRXVirtualCode primary .tsx service script |
text + extension: '.tsx' |
reuse the type-only transform unchanged |
mappings[] with sourceOffsets/generatedOffsets/lengths/generatedLengths |
SpanMapping tuples |
equal length and equal text → Verbatim; otherwise Atom; obfuscated identifiers (_$_ prefix, identifier-utils.js) → Alias so diagnostics show the authored name |
mapping_data (verification, completion, semantic, navigation, structure, format:false) |
feature bits | completion→Completion; semantic→Hover, SignatureHelp, InlayHints, SemanticTokens; navigation→Definition, TypeDefinition, Implementation, References, Rename, CallHierarchy, DocumentHighlights; structure→DocumentSymbols, FoldingRanges, SelectionRanges; format:false→omit Formatting. verification has no bit: diagnostics always apply |
mapping_data_string_span (shouldHighlight: () => false) |
omit SemanticTokens and DocumentHighlights bits | the only function-valued policy in the codebase |
mapping_data_completion_only |
Completion bit only | note: diagnostics will still be reported on these spans; confirm this is acceptable per site |
embedded <script> TS bodies (getExtraServiceScripts, omitted under tsrx-tsc today) |
supplemental outputs |
explicit decision, tested; this widens CLI coverage relative to today |
embedded <style> CSS |
not TypeScript's concern | stays in the TSRX language server |
customData.hover / customData.definition (CSS class spans) |
TSRX server providers | leave Hover/Definition bits off on those spans so TS does not also answer |
customData.wordHighlight |
TSRX server document-highlight provider | see open question |
compiler selection (tsrx.compiler, extends chain, auto-detect) |
openProject receives configFileName and effective compilerOptions; TSRX reads its own config |
one precedence rule between contentMappers[].options and the existing tsrx tsconfig field |
| cache invalidation on config/package change | dynamicConfig + configIdentity + watchedFiles |
include the full extends chain, the compiler package's package.json, and any file that changes transform output |
Compile-failure fallback (design decision, not derivable from #38). Today, when the TSRX compile fails, language.js feeds the raw source back as the generated text with a 1:1 mapping so TS points at the broken construct, and the diagnostic plugin suppresses every TS diagnostic while fatalErrors is non-empty. Under the native protocol nothing can suppress TS diagnostics, so raw TSRX source parsed as TSX would produce a wall of syntax errors. Recommended: report the compile error through diagnostics, and return a stub text that preserves the file's export surface (from the last successful transform when available, otherwise a minimal export {}) with an empty span map. Importers keep resolving; the author sees one error at the right place. Evaluate against the completion-during-broken-edit requirement, which moves entirely to the TSRX server's snippet provider.
Open question, provider merging in VS Code. Hover, completion, and definition providers from multiple extensions merge. Document highlights historically use the highest-scoring single provider; the native extension registers a multi-document-highlight feature. Verify keyword highlights from the TSRX server coexist with TS highlights before relying on it.
Editor architecture
VS Code
- The native TypeScript extension (
TypescriptTeam.vscode-typescript) owns TypeScript features on.tsrx: hover, completions, signature help, definitions, references, rename, code actions, auto-import, diagnostics, inlay hints, semantic tokens. For configured projects this happens from tsconfig alone. Requires thejs/ts.contentMappers.enabledsetting (defaulttrue, experimental) and a trusted workspace. - For inferred projects, the TSRX extension calls
api.registerContentMappers('TSRX.tsrx-vscode-plugin', [{ extensions: ['.tsrx'], inferredProjectContribution: { options, manifest: { name, exec: [process.execPath, <bundled server.js>], dynamicConfig: true } } }])after activating the native extension. Pattern: mdx-content-mappervscode/src/extension.ts. - The TSRX extension keeps a slimmed language server for what TypeScript does not own: snippet completions (target-gated), CSS in
<style>, document symbols, auto-insert, CSS-class hover and definition, keyword highlights, TSRX formatting, and compile-error diagnostics only if the mapper'sdiagnosticsdo not already cover them. - Remove from the extension:
volar-service-typescript, the TypeScript-wrapping plugins, the runtime patching of the built-invscode.typescript-language-featuresextension (extension.jsaround lines 532 to 603), and theTSGO_CONFIGURATION_SECTIONShandling. - Keep the classic path selectable during transition behind one documented setting. Never run both TS backends on the same file.
Other editors
- Document: run TypeScript 7's LSP with
initializationOptions.runExternalCode: true, declare the mapper in tsconfig, keep the TSRX language server for the non-TypeScript features listed above. Verify per editor which features the two servers provide and that nothing is duplicated. - Track Push per-file diagnostics for clients without pull diagnostics support microsoft/TypeScript#63921 (push diagnostics for clients without pull-diagnostic support); some clients will need it.
Phases
Phase 0, baseline
- Add
[email protected](or a later verified nightly) as an exact devDependency, distinct from the classictypescriptcatalog pin. Document how the native binary is located from the platform package. - Run
pnpm test --project typescript-pluginand--project language-serverat the starting commit; record pre-existing failures. - Add a consumer fixture: two
.tsrxmodules importing each other, a.tsimporter, one intentional cross-file prop-type error, one embedded<script>block, one<style>block with a scoped class. Capture classictsrx-tscoutput (positions, codes, exit status) as the parity reference. - Inventory every mapping site by
mapping_datavariant and by whether generated length equals source length. This is the list that becomes Verbatim/Atom/Alias assignments. - Delete the dead
suppressedDiagnosticsplumbing.
Exit: reproducible nightly install, fixture, and a complete mapping inventory.
Phase 1, mapper package @tsrx/content-mapper
- Factor the transform out of
TSRXVirtualCodeinto a pure function with no Volar types:(fileName, content, compiler, compilerOptions) → { text, mappings, embeddedScripts, cssRegions, compileErrors }.language.jsand the mapper both consume it. - Implement
initialize(UTF-16),openProject,transform,closeProjectover stdio withContent-Lengthframing. Per-project state keyed byprojectHandle; no cache leakage between projects; stdout reserved for protocol. - Reuse
consumer-compiler.jsandtsconfig-resolution.jsfor compiler selection. Audit their classic-TypeScript imports (JSONC parsing,extendsresolution); either keep an explicitly versioned classic dependency for that purpose or replace it. Do not claim independence from classic TS while relying on it. - Define precedence:
contentMappers[].options.compilervstsrx.compilerin tsconfig. One rule, documented. - Convert mappings per the table above. Normalize overlapping generated spans into a disjoint span map. Test exact boundaries, zero-length anchors, adjacent spans, CRLF, astral Unicode, non-ASCII before mapped positions.
- Emit
Aliasfor obfuscated identifiers; verify diagnostic text shows authored names. - Emit embedded
<script>bodies assupplementaloutputs. - Emit compile errors via
diagnostics; implement the compile-failure fallback decided above. -
dynamicConfigcontract:configIdentityfrom theextendschain, the resolved compiler package version, andtsrxoptions;watchedFilescovering each of those files. - Integration tests that spawn the pinned native
tsc --runExternalCodeagainst the fixture and assert file, range, code, and exit status. Required in CI; a missing binary fails the job rather than skipping.
Exit: native tsc checks the fixture across React, Preact, Solid, Vue, and a Ripple consumer, resolves .tsrx imports, and reports the intentional error at the authored span.
Phase 2, CLI, build, declarations
- Document the opt-in: tsconfig
contentMappersentry plustsc --runExternalCode. Keeptsrx-tscunchanged for the classic path. - Test
--noEmit,--watch, incremental no-op rebuilds,--buildwith project references, file add/delete/rename, and clean rebuilds after tsconfig or compiler-package changes. - Declaration emit: verify
Component.d.tsrx.tsnaming, declaration maps, helper imports, generics, package exports, and consumption from a separate project. Re-typecheck emitted declarations as plain TS. - Track Add the outputExtension compiler option microsoft/TypeScript#64120 (
outputExtension); adopt when merged. - Respect
--runExternalCodeas a user decision. Never enable it implicitly.
Phase 3, editor
- VS Code: inferred-project registration through
registerContentMappers; activation when only a.tsrxfile is open; disposal on deactivate. - Provider ownership per feature as listed under "Editor architecture". Resolve the document-highlight merging question with a real test.
- Slim the TSRX language server; remove
volar-service-typescriptand the TypeScript-wrapping plugins from the native path. - Test auto-import both extending an existing import and inserting a new one (Auto-imports don’t work in content-mappers without existing import microsoft/TypeScript#64119 is open for the latter). Test rename on Atom spans (feat(contentmapper): support whole-symbol rename edit projection microsoft/TypeScript#63879: not supported in 7.1 baseline; document the limitation for any renamed generated identifiers).
- Test untrusted workspaces (no mapper code runs), native extension absent, server restart, multi-root, and switching back to the classic backend.
- Write the per-editor guide for Zed, Neovim, IntelliJ, Sublime.
Phase 4, rollout
- Benchmark against the classic path on the same fixture and hardware: cold check, warm check, single-edit latency, peak memory, mapper process count. Report repeated measurements.
- Compatibility matrix with every difference classified: compiler-version difference, previously hidden real error, adapter bug, upstream limitation.
- Release notes, migration and rollback instructions, explicit decision on the default backend. Deprecating the classic tooling is a separate decision.
Validation matrix
| Dimension | Required evidence |
|---|---|
| Targets | React, Preact, Solid, Vue; external Ripple; one third-party compiler fixture |
| Type safety | good programs pass; intentional prop, import, generic, and control-flow errors fail with correct file, range, code, exit status |
| TSRX syntax | template bodies, @if/@for/@switch/@try, <script> and <style> embeddings, incomplete edits |
| Resolution | mixed .ts/.tsx/.tsrx, package exports, aliases, project references, nested configs, monorepo symlinks |
| Editor | hover, completions, signature help, definitions, references, rename, code actions, auto-import, diagnostics, symbols, highlights, CSS features |
| Lifecycle | repeated open/edit/close, multiple projects per mapper process, deletion and rename, restart, trusted and untrusted workspaces |
| Packaging | installed tarball and extension work outside the checkout on macOS, Linux, Windows |
Commands (reconfirm at implementation time):
pnpm test --project typescript-plugin
pnpm test --project language-server
pnpm test --project vscode-plugin
pnpm typecheck
pnpm changeset:check
pnpm format:checkUpstream items to track
- feat(contentmapper): support whole-symbol rename edit projection microsoft/TypeScript#63879, whole-symbol rename projection for Atom spans (not in 7.1 baseline)
- Auto-imports don’t work in content-mappers without existing import microsoft/TypeScript#64119, auto-import insertion when no import statement exists
- Add the outputExtension compiler option microsoft/TypeScript#64120 and #64053,
outputExtensionand declaration naming - API feature roadmap microsoft/TypeScript#63875 item 1B, LSP client middleware (would allow hover-text rewriting; cosmetic for TSRX)
- Push per-file diagnostics for clients without pull diagnostics support microsoft/TypeScript#63921, push diagnostics for clients without pull support
- feat: content mapper (v4 alpha) vuejs/language-tools#6170, reference implementation and test harness
Non-goals
Rewriting TSRX in Go; replacing runtime or bundler compilation; using the unstable/* semantic API (not needed); immediately removing the classic path; changing default tooling for every consumer in the first release.
Alternatives considered
- Fork or extend Volar to speak the protocol. The transport is roughly 150 lines; the framework-specific work (span kinds, supplemental outputs, compile-failure behavior) cannot be generic. Volar's TypeScript host layer is what the protocol replaces, and upstream is not adapting it. Rejected.
- Wait for the native semantic API. TSRX has no consumer for it. Rejected.
- Native-only editor support, drop the TSRX language server. Snippets, CSS, symbols, auto-insert, and TSRX formatting have no TypeScript equivalent. Rejected; the server stays, slimmed.
Checklist
- I've searched for existing feature requests (supersedes [Feature]: Add native TypeScript 7 support via content mappers (CLI and editor integration) #38)
- I'm willing to help implement this feature
Activity
fix(content-mapper): keep type modifiers on re-exports in the compile…
fix(content-mapper): keep the last-good AST across a project reopen (#41
docs(content-mapper): link the filed upstream issues for TS6307 and -…
feat(typescript-plugin): support TypeScript 6 and explain the TypeScr…
test(content-mapper): close the validation gaps of the compatibility …
docs: TypeScript 7 requirements, status and setup on tsrx.dev and in …
feat(typescript-plugin): the native TypeScript 7 path needs no other …
fix(typescript-plugin): append .json to a relative extends path besid…
fix(typescript-plugin): keep jsonc-parser external so tsrx-tsc loads (#…
feat(typescript-plugin): tsrx-tsc runs native tsc --runExternalCode o…
fix(typescript-plugin): tsrx-tsc checks the --build references graph …
fix(typescript-plugin): a --build config with files: [] and an includ…
feat(vscode-plugin): native backend without the TypeScript 7 extensio…
24 remaining items
fix(vscode-plugin): open the wake-up file as an inactive tab beside t…
fix(vscode-plugin): pin the wake-up tab and wait for a TypeScript dia…
chore(content-mapper): version 0.1.0 for the first manual publish (#41)
chore(content-mapper): restore the changeset entry (#41)
docs(content-mapper): the classic path stays while TypeScript 5.9 and…
chore: minor bumps for the TypeScript 7 changesets; core and content-…
fix(content-mapper): bundle the package exports and quote string expo…
fix(vscode-plugin): activate TypeScript through extension APIs (#41)
fix(content-mapper): keep code 1000 for fatal errors now that every e…
fix(vscode-plugin): drop the server's copy of a mistake tsserver also…
feat(content-mapper): send TSRX codes as their numbers and leave Type…
feat(content-mapper): report errors under the source `TSRX`, in capit…
feat(content-mapper): send every compile error, with a directive that…
Native TypeScript 7 support for .tsrx through a content mapper (#41) (#…