Skip to content

feat(content-mapper): teach tsc and the TS language server to read .svelte - #3379

Merged
baseballyama merged 3 commits into
mainfrom
feat/content-mapper
Aug 24, 2026
Merged

feat(content-mapper): teach tsc and the TS language server to read .svelte#3379
baseballyama merged 3 commits into
mainfrom
feat/content-mapper

Conversation

@baseballyama

Copy link
Copy Markdown
Owner

What

crates/rsvelte_content_mapper — a content mapper for .svelte, so tsc and the
TypeScript language server read components directly instead of a tool materialising .tsx
shadows 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 the
mapper 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-Length framing, UTF-8 positions (rsvelte's offsets are bytes, so any
other encoding buys a conversion per span). svelte2tsx's forward_map is already equal-length
copies, which is exactly SpanMapKind.Verbatim; its Ωignore regions become Ignore
diagnostic directives — the port of dropping those diagnostics after the fact.

This is not a speed feature. Type checking is 66-89% of an rsvelte-check run (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.1 on a 200-component project:

result
without globalTypes 599 TS2304 + 200 TS2688
with them clean, exit 0
error injected in a <script> Comp7.svelte(2,7): TS2322 — the count position
error injected in a template expression Comp9.svelte(7,31): TS2551 — the toFixed position

The 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.

  • Serialization was the wrong worry. With an artificial 20/50 ms delay per transform,
    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::scope around the read loop 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. every_request_is_answered_even_when_transforms_finish_out_of_order is the
    test that catches it.
  • A crashing mapper is loud. Five failures produce TS100026, a TS100025 per affected
    file, 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 own
types="svelte" directive is blanked with spaces so the offsets after it do not move. Both
have 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-check keeps the overlay path as its only path, and no published
package is touched, so this cannot regress the check/LSP parity gates.

What is not settled, and blocks making this the default:

  • unmappable-region diagnostics are reported rather than dropped, and the protocol cannot filter
    by diagnostic code while mapper.rs does — a designed-in parity difference against official
    svelte-check
  • the overlay path must stay for TS 6 and TS 7.0, which have no content mappers
  • preprocessors would run inside the mapper, putting a Node sidecar under a Rust binary
  • panic = "abort" in the release profile means a panic takes the whole mapper process down, so
    every failure path has to be a Result (a broken <script> body is deliberately passed
    through for TypeScript to report, rather than spending one of the five allowed failures)

Testing

14 unit tests. cargo fmt --all -- --check and
cargo clippy --all-targets --all-features -- -D warnings both exit 0 — run out of band, since
the pre-commit hook outruns the session's command timeout on a cold target.

…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
Copilot AI lite review requested due to automatic review settings August 22, 2026 02:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@baseballyama
baseballyama merged commit 8ea1a5e into main Aug 24, 2026
8 of 9 checks passed
@baseballyama
baseballyama deleted the feat/content-mapper branch August 24, 2026 16:35
@codspeed-hq

codspeed-hq Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 11 untouched benchmarks
⏩ 308 skipped benchmarks1


Comparing feat/content-mapper (663aebe) with main (019a8a3)

Open in CodSpeed

Footnotes

  1. 308 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants