Skip to content

[Feature]: Native TypeScript 7 support via content mappers, revised plan (supersedes #38) #41

Closed

Description

@leonidaz

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?] plus unusedExpectDirectiveDiagnostics.
  • supplemental: extra virtual files, each { text, extension, mappings, diagnosticDirectives? }, named <file>.<index><ext> by the compiler. .mts/.cts/.mjs/.cjs extensions force module scope.
  • extension: one of .js .jsx .mjs .cjs .ts .tsx .mts .cts .json.

Reference implementations

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 the js/ts.contentMappers.enabled setting (default true, 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-mapper vscode/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's diagnostics do not already cover them.
  • Remove from the extension: volar-service-typescript, the TypeScript-wrapping plugins, the runtime patching of the built-in vscode.typescript-language-features extension (extension.js around lines 532 to 603), and the TSGO_CONFIGURATION_SECTIONS handling.
  • Keep the classic path selectable during transition behind one documented setting. Never run both TS backends on the same file.

Other editors

Phases

Phase 0, baseline

  • Add [email protected] (or a later verified nightly) as an exact devDependency, distinct from the classic typescript catalog pin. Document how the native binary is located from the platform package.
  • Run pnpm test --project typescript-plugin and --project language-server at the starting commit; record pre-existing failures.
  • Add a consumer fixture: two .tsrx modules importing each other, a .ts importer, one intentional cross-file prop-type error, one embedded <script> block, one <style> block with a scoped class. Capture classic tsrx-tsc output (positions, codes, exit status) as the parity reference.
  • Inventory every mapping site by mapping_data variant and by whether generated length equals source length. This is the list that becomes Verbatim/Atom/Alias assignments.
  • Delete the dead suppressedDiagnostics plumbing.

Exit: reproducible nightly install, fixture, and a complete mapping inventory.

Phase 1, mapper package @tsrx/content-mapper

  • Factor the transform out of TSRXVirtualCode into a pure function with no Volar types: (fileName, content, compiler, compilerOptions) → { text, mappings, embeddedScripts, cssRegions, compileErrors }. language.js and the mapper both consume it.
  • Implement initialize (UTF-16), openProject, transform, closeProject over stdio with Content-Length framing. Per-project state keyed by projectHandle; no cache leakage between projects; stdout reserved for protocol.
  • Reuse consumer-compiler.js and tsconfig-resolution.js for compiler selection. Audit their classic-TypeScript imports (JSONC parsing, extends resolution); 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.compiler vs tsrx.compiler in 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 Alias for obfuscated identifiers; verify diagnostic text shows authored names.
  • Emit embedded <script> bodies as supplemental outputs.
  • Emit compile errors via diagnostics; implement the compile-failure fallback decided above.
  • dynamicConfig contract: configIdentity from the extends chain, the resolved compiler package version, and tsrx options; watchedFiles covering each of those files.
  • Integration tests that spawn the pinned native tsc --runExternalCode against 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 contentMappers entry plus tsc --runExternalCode. Keep tsrx-tsc unchanged for the classic path.
  • Test --noEmit, --watch, incremental no-op rebuilds, --build with project references, file add/delete/rename, and clean rebuilds after tsconfig or compiler-package changes.
  • Declaration emit: verify Component.d.tsrx.ts naming, 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 --runExternalCode as a user decision. Never enable it implicitly.

Phase 3, editor

  • VS Code: inferred-project registration through registerContentMappers; activation when only a .tsrx file 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-typescript and 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:check

Upstream items to track

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

Activity

  1. self-assigned this
    on Sep 18, 2026
  2. 24 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions