feat(content-mapper): teach tsc and the TS language server to read .svelte - #3379
Merged
Conversation
…velte A content mapper is an external process TypeScript spawns during program construction (microsoft/typescript-go#4712, merged 2026-08-19): it hands the mapper a file it cannot parse and gets back TypeScript text plus a span map, then reports its diagnostics at the original coordinates. That is what lets `tsc` read `.svelte` directly instead of a tool materialising `.tsx` shadows beside every component. This is protocol version 1 over stdio, UTF-8 positions, with svelte2tsx's `forward_map` as the verbatim span map and its `Ωignore` regions as `Ignore` diagnostic directives. It is not a speed feature: type checking is 66-89% of a check run, and this changes how files enter the program, not how long checking them takes. Transforms run inside a `rayon::scope` because TypeScript pipelines — measured at 200 of 200 requests outstanding at once, up to 119 per write — so anything that serialises here is ours. The scope is load-bearing rather than tidy: without it, end of stdin returns from `serve` while transforms are still queued and their replies are never written. A mapper cannot add a file to the program, and in the language server the user's tsconfig is not ours to edit, so the ambient declarations svelte2tsx output depends on arrive as a `/// <reference path>` prefix from `options.globalTypes`. That prefix shifts every span, and svelte2tsx's own `types="svelte"` directive is blanked with spaces so the offsets after it do not move. Verified end to end against tsc 7.1.0-dev.20260821.1 on 200 components: 599 TS2304 + 200 TS2688 without the shims, clean with them, and an injected error in a script and in a template expression both land on the original line and column. Pre-commit checks were run out of band (`cargo fmt --all -- --check` and `cargo clippy --all-targets --all-features -- -D warnings`, both exit 0); the hook itself outruns this session's command timeout on a cold target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DWAZjvynCj6ApFwf75VXN
Contributor
Merging this PR will not alter performance
Comparing Footnotes
|
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.
What
crates/rsvelte_content_mapper— a content mapper for.svelte, sotscand theTypeScript language server read components directly instead of a tool materialising
.tsxshadows beside every one of them.
Content mappers landed in microsoft/typescript-go#4712
(merged 2026-08-19) and are live as of
typescript@7.1.0-dev.20260821.1. TypeScript spawns themapper during program construction, hands it a file it cannot parse, and gets back TypeScript
text plus a span map; diagnostics then land on the original coordinates.
This is protocol version 1 over stdio —
initialize/openProject/transform/closeProject,Content-Lengthframing, UTF-8 positions (rsvelte's offsets are bytes, so anyother encoding buys a conversion per span). svelte2tsx's
forward_mapis already equal-lengthcopies, which is exactly
SpanMapKind.Verbatim; itsΩignoreregions becomeIgnorediagnostic directives — the port of dropping those diagnostics after the fact.
This is not a speed feature. Type checking is 66-89% of an
rsvelte-checkrun (measured in#3369), and a content mapper changes how files enter the program, not how long checking them
takes. It is a product and maintenance play.
Verified end to end
Against the real
tsc 7.1.0-dev.20260821.1on a 200-component project:globalTypesTS2304+ 200TS2688<script>Comp7.svelte(2,7): TS2322— thecountpositionComp9.svelte(7,31): TS2551— thetoFixedpositionThe last two are the negative control: "0 diagnostics" is also what an empty program reports.
Two design hazards, falsified by measurement
Both are structural counts, not timings, so a loaded machine could not fake them.
200 of 200 requests were outstanding at once and up to 119 arrived in a single write.
TypeScript pipelines everything, so whatever serialises is the mapper's own doing — hence the
rayon pool. The
rayon::scopearound the read loop is load-bearing rather than tidy: withoutit, end of stdin returns from
servewhile transforms are still queued and their replies arenever written.
every_request_is_answered_even_when_transforms_finish_out_of_orderis thetest that catches it.
TS100026, aTS100025per affectedfile, and exit 2. The residual hole is per file rather than per run: once the mapper is
disabled the remaining components are treated as empty TypeScript, so their diagnostics
vanish while the run still fails.
Protocol overhead is small: 200 transforms, 54 KiB of payload, served in an 8.2 ms window.
The shim problem, and how it is solved here
A content mapper cannot add a file to the program, and in the language server the user's
tsconfig is not ours to edit — so the ambient declarations svelte2tsx output depends on
(
__sveltets_2_*, the JSX namespace) arrive as a/// <reference path>prefix per virtual file,from
contentMappers[].options.globalTypes. That prefix shifts every span, and svelte2tsx's owntypes="svelte"directive is blanked with spaces so the offsets after it do not move. Bothhave a test that reconstructs each span's text from the output and compares it to the original,
so a desynchronised map fails rather than merely looking plausible.
Scope
Nothing else changes.
rsvelte-checkkeeps the overlay path as its only path, and no publishedpackage is touched, so this cannot regress the check/LSP parity gates.
What is not settled, and blocks making this the default:
by diagnostic code while
mapper.rsdoes — a designed-in parity difference against officialsvelte-check
panic = "abort"in the release profile means a panic takes the whole mapper process down, soevery failure path has to be a
Result(a broken<script>body is deliberately passedthrough for TypeScript to report, rather than spending one of the five allowed failures)
Testing
14 unit tests.
cargo fmt --all -- --checkandcargo clippy --all-targets --all-features -- -D warningsboth exit 0 — run out of band, sincethe pre-commit hook outruns the session's command timeout on a cold target.