Skip to content

feat: typescript 7 content mapper - #3111

Open
jasonlyu123 wants to merge 55 commits into
sveltejs:masterfrom
jasonlyu123:ts7-content-mapper
Open

jasonlyu123 wants to merge 55 commits into
sveltejs:masterfrom
jasonlyu123:ts7-content-mapper

Conversation

@jasonlyu123

@jasonlyu123 jasonlyu123 commented Aug 22, 2026 •

Copy link
Copy Markdown
Member

#2733

The language server part currently requires changes in the TypeScript 7 VSCode extension that haven't been published yet. This can only be tested with a local TypeScript extension build.

This PR is focused on editor support. Svelte-check support will be a separate PR.

Known problems

  • inlay hints
    Duplicated entries. Opened an upstream issue and also reduced the places where it happens.
  • document highlight
    VS Code doesn't seem to allow multiple providers. Need to manually merge it when we have a middleware api.
  • SvelteKit auto-typing
  • Some Svelte 4 legacy features were unmappable and currently can't be filtered out.

PR Test instruction

(Will edit this after this change is published)

  • Install the TypeScript 7 extension and the TypeScript 7 nightly extension. tsdk config should also work but svelte-check --tsgo-experimental-api isn't compatiable with the latest nightly version.So you need to avoid workspace tsdk config.
  • Install content mapper and svelte2tsx using a relative path to a test Svelte project.
{
  "devDependencies": {
  	"@sveltejs/ts-content-mapper": "file:../language-tools/packages/typescript-content-mapper"
  },
  "pnpm": {
  	"overrides": {
  		"@sveltejs/load-config": "file:../language-tools/packages/load-config",
  		"svelte2tsx": "file:../language-tools/packages/svelte2tsx"
  	}
  }
  • Add this to the tsconfig.json of the test project
	"contentMappers": [
  	{
  		"package": "@sveltejs/ts-content-mapper",
  		"extensions": [".svelte"]
  	}
  ]

@changeset-bot

changeset-bot Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d3766bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
svelte-language-server Patch
svelte2tsx Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@andrewbranch

Copy link
Copy Markdown

Exciting progress! Do you have a stack trace for the auto-import panic handy?

@jasonlyu123

Copy link
Copy Markdown
Member Author

Do you have a stack trace for the auto-import panic handy?

Debug failure. False expression: Token end is child end
runtime/debug.Stack()
	C:/Program Files/Go/src/runtime/debug/stack.go:26 +0x8e
github.com/microsoft/TypeScript/tsc/internal/lsp.(*Server).recover(0x25835d125508, 0x25836906e3f0)
	C:/projects/TypeScript/tsc/internal/lsp/server.go:1477 +0x4f
panic({0x7ff7b8bc3580?, 0x2583610277f0?})
	C:/Program Files/Go/src/runtime/panic.go:860 +0x14f
github.com/microsoft/TypeScript/tsc/internal/debug.Fail({0x258368c81bc0, 0x37})
	C:/projects/TypeScript/tsc/internal/debug/debug.go:14 +0x65
github.com/microsoft/TypeScript/tsc/internal/debug.assertSlow({0x258368d9cde8, 0x1, 0x1})
	C:/projects/TypeScript/tsc/internal/debug/debug.go:60 +0xa5
github.com/microsoft/TypeScript/tsc/internal/debug.Assert(0x0, {0x258368d9cde8, 0x1, 0x1})
	C:/projects/TypeScript/tsc/internal/debug/debug.go:49 +0x3b
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).processChildNode(0x25835f7505a0, 0x258369118000, 0x258369115400, 0x0, 0x0, 0x25836205f6c8, 0xffffffffffffffff, 0x258369118000, 0x258369115400, 0x0, ...)
	C:/projects/TypeScript/tsc/internal/format/span.go:410 +0x957
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).execute.func1(0x25836205f6c8)
	C:/projects/TypeScript/tsc/internal/format/span.go:236 +0x15a
github.com/microsoft/TypeScript/tsc/internal/ast.(*NodeVisitor).VisitNode(0x2583690fb620, 0x25836205f6c8)
	C:/projects/TypeScript/tsc/internal/ast/visitor.go:51 +0x87
github.com/microsoft/TypeScript/tsc/internal/ast.(*NodeVisitor).visitNode(0x2583690fb620, 0x25836205f6c8)
	C:/projects/TypeScript/tsc/internal/ast/visitor.go:198 +0x6a
github.com/microsoft/TypeScript/tsc/internal/ast.(*ImportDeclaration).VisitEachChild(0x258369118000, 0x2583690fb620)
	C:/projects/TypeScript/tsc/internal/ast/ast_generated.go:2564 +0xa5
github.com/microsoft/TypeScript/tsc/internal/ast.(*Node).VisitEachChild(0x258369118000, 0x2583690fb620)
	C:/projects/TypeScript/tsc/internal/ast/ast.go:208 +0x42
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).executeProcessNodeVisitor(0x25835f7505a0, 0x258369118000, 0x258369115400, 0x0, 0x0)
	C:/projects/TypeScript/tsc/internal/format/span.go:537 +0x125
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).processNode(0x25835f7505a0, 0x258369118000, 0x2583690f4388, 0x0, 0x0, 0x4, 0x4)
	C:/projects/TypeScript/tsc/internal/format/span.go:638 +0x156
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).processChildNode(0x25835f7505a0, 0x2583690f4388, 0x258369115360, 0x0, 0x0, 0x258369118000, 0xffffffffffffffff, 0x2583690f4388, 0x258369115360, 0x0, ...)
	C:/projects/TypeScript/tsc/internal/format/span.go:428 +0xb05
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).processChildNodes(0x25835f7505a0, 0x2583690f4388, 0x258369115360, 0x0, 0x0, 0x2583619c9140, 0x2583690f4388, 0x0, 0x258369115360)
	C:/projects/TypeScript/tsc/internal/format/span.go:501 +0x36c
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).execute.func2(0x2583619c9140, 0x2583690fb620)
	C:/projects/TypeScript/tsc/internal/format/span.go:243 +0x10d
github.com/microsoft/TypeScript/tsc/internal/ast.(*NodeVisitor).visitNodes(0x2583690fb620, 0x2583619c9140)
	C:/projects/TypeScript/tsc/internal/ast/visitor.go:234 +0x4b
github.com/microsoft/TypeScript/tsc/internal/ast.(*NodeVisitor).visitTopLevelStatements(0x2583690fb620, 0x2583619c9140)
	C:/projects/TypeScript/tsc/internal/ast/visitor.go:257 +0x6a
github.com/microsoft/TypeScript/tsc/internal/ast.(*SourceFile).VisitEachChild(0x2583690f4388, 0x2583690fb620)
	C:/projects/TypeScript/tsc/internal/ast/ast.go:2798 +0x45
github.com/microsoft/TypeScript/tsc/internal/ast.(*Node).VisitEachChild(0x2583690f4388, 0x2583690fb620)
	C:/projects/TypeScript/tsc/internal/ast/ast.go:208 +0x42
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).executeProcessNodeVisitor(0x25835f7505a0, 0x2583690f4388, 0x258369115360, 0x0, 0x0)
	C:/projects/TypeScript/tsc/internal/format/span.go:537 +0x125
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).processNode(0x25835f7505a0, 0x2583690f4388, 0x2583690f4388, 0x0, 0x0, 0x4, 0x4)
	C:/projects/TypeScript/tsc/internal/format/span.go:638 +0x156
github.com/microsoft/TypeScript/tsc/internal/format.(*formatSpanWorker).execute(0x25835f7505a0, 0x25836437d130)
	C:/projects/TypeScript/tsc/internal/format/span.go:257 +0x593
github.com/microsoft/TypeScript/tsc/internal/format.newFormattingScanner({0x258364ff7800, 0x2f}, 0x0, 0x0, 0x2f, 0x25835f7505a0)
	C:/projects/TypeScript/tsc/internal/format/scanner.go:57 +0x189
github.com/microsoft/TypeScript/tsc/internal/format.FormatNodeGivenIndentation({0x7ff7b9033b98, 0x258368dfc1b0}, 0x2583690f4388, 0x2583690f4388, 0x0, 0x4, 0x4)
	C:/projects/TypeScript/tsc/internal/format/api.go:83 +0x15f
github.com/microsoft/TypeScript/tsc/internal/ls/change.(*Tracker).getFormattedTextOfNode(0x2583678a5b20, 0x2583650558f0, 0x25836888a708, 0x25836888a708, 0x1f6, {{0x0, 0x0}, {0x7ff7b901e6e8, 0x1}, 0x0, ...})
	C:/projects/TypeScript/tsc/internal/ls/change/trackerimpl.go:140 +0x4b3
github.com/microsoft/TypeScript/tsc/internal/ls/change.(*Tracker).computeNewText.func1(0x2583650558f0)
	C:/projects/TypeScript/tsc/internal/ls/change/trackerimpl.go:86 +0x112
github.com/microsoft/TypeScript/tsc/internal/ls/change.(*Tracker).computeNewText(0x2583678a5b20, 0x2583690f3400, 0x25836888a708, 0x25836888a708)
	C:/projects/TypeScript/tsc/internal/ls/change/trackerimpl.go:98 +0x2d7
github.com/microsoft/TypeScript/tsc/internal/ls/change.(*Tracker).getTextChangesFromChanges.func2(0x2583690f3400)
	C:/projects/TypeScript/tsc/internal/ls/change/trackerimpl.go:43 +0x4b
github.com/microsoft/TypeScript/tsc/internal/core.MapNonNil[...]({0x2583690b53e8, 0x1, 0x1}, 0x258368d9e920)
	C:/projects/TypeScript/tsc/internal/core/core.go:120 +0xd9
github.com/microsoft/TypeScript/tsc/internal/ls/change.(*Tracker).getTextChangesFromChanges(0x2583678a5b20)
	C:/projects/TypeScript/tsc/internal/ls/change/trackerimpl.go:40 +0x2de
github.com/microsoft/TypeScript/tsc/internal/ls/change.(*Tracker).GetChanges(0x2583678a5b20)
	C:/projects/TypeScript/tsc/internal/ls/change/tracker.go:137 +0x65
github.com/microsoft/TypeScript/tsc/internal/ls/autoimport.fileEdits(0x2583678a5b20, 0x25836888a708)
	C:/projects/TypeScript/tsc/internal/ls/autoimport/fix.go:134 +0x66
github.com/microsoft/TypeScript/tsc/internal/ls/autoimport.(*Fix).Edits(_, {_, _}, _, _, {{0x0, 0x4, 0x4, {0x7ff7b901e6e8, 0x1}, ...}, ...}, ...)
	C:/projects/TypeScript/tsc/internal/ls/autoimport/fix.go:109 +0xe66
github.com/microsoft/TypeScript/tsc/internal/ls.(*LanguageService).filterContentMappedAutoImports(0x258361516b40, {0x7ff7b9033b98, 0x258367fdc960}, 0x25835f17fb88, 0x25836888a708, 0x258368dfc150)
	C:/projects/TypeScript/tsc/internal/ls/completions.go:89 +0x44f
github.com/microsoft/TypeScript/tsc/internal/ls.(*LanguageService).ProvideCompletion(0x258361516b40, {0x7ff7b9033b98, 0x258367fdc960}, {0x258361234000, 0x44}, {0x8, 0x2}, 0x258363ef35a0)
	C:/projects/TypeScript/tsc/internal/ls/completions.go:70 +0x51c
github.com/microsoft/TypeScript/tsc/internal/lsp.(*Server).handleCompletion(0x25835d125508, {0x7ff7b9033b98, 0x258367fdc870}, 0x258361516b40, 0x258367fdc8a0)
	C:/projects/TypeScript/tsc/internal/lsp/server.go:2112 +0xe5
github.com/microsoft/TypeScript/tsc/internal/lsp.registerLanguageServiceWithAutoImportsRequestHandler[...].func1.1.1()
	C:/projects/TypeScript/tsc/internal/lsp/server.go:1374 +0x174
github.com/microsoft/TypeScript/tsc/internal/project.(*Session).WithLanguageServiceAndSnapshot.func1()
	C:/projects/TypeScript/tsc/internal/project/session.go:1336 +0xb1
github.com/microsoft/TypeScript/tsc/internal/lsp.(*Server).handleRequestOrNotification.func1()
	C:/projects/TypeScript/tsc/internal/lsp/server.go:1167 +0x75
github.com/microsoft/TypeScript/tsc/internal/lsp.(*Server).dispatchLoop.func3()
	C:/projects/TypeScript/tsc/internal/lsp/server.go:1014 +0x33
created by github.com/microsoft/TypeScript/tsc/internal/lsp.(*Server).dispatchLoop in goroutine 10
	C:/projects/TypeScript/tsc/internal/lsp/server.go:1013 +0x81f

Weirdly, in getFormattedTextOfNode, the text returned in t.getNonformattedText is a broken import statement.

import { SvelteComponentTyped } from te-tsgo-t;

And if I track down the module specifier printing in that function, in the getLiteralTextOfNode function here, p.emitContext.textSource is nil. It sounds like the emitContext might not be the same one when the node is created?

Btw, should I open an issue to track this? There is also a problem with duplicated inlay hints that seems to happen when a node is split into multiple spans. Should I open two separate issues?

@andrewbranch

Copy link
Copy Markdown

Two issues would be great, if you don't mind!

@andrewbranch

Copy link
Copy Markdown

Hey @jasonlyu123! Would love to get your input on two things:

  • We've been discussing potential problems with synchronizing LSP state between TS and other LSP servers that use the TS API and get its state via snapshots—in particular, how to avoid the pitfall of a race like

    1. Svelte server gets textDocument/didChange for event A
    2. Svelte server gets textDocument/diagnostics (or something)
    3. Svelte server gets snapshot from TS to service request
    4. TS server gets textDocument/didChange for event A
    5. TS server updates its snapshot

    such that Svelte's own document state is ahead of TypeScript's. The two mechanisms we've proposed to help mitigate this are

    • an api.on("snapshotUpdate", ...) event emitter so you can ensure you always react to snapshot changes rather than accidentally preempt them
    • including the LSP document versions in snapshots. At least in VS Code, with vscode-languageclient, versions are synchronized between LSP clients, so that would let you reason about whether your own document state is older, equal, or newer than that in any given TS snapshot.
  • Secondly, I think this PR description is the first specific use case or request for an LSP middleware, which I knew we would need but have been surprised nobody else has been actively asking for yet.

We haven't hammered out the details on these, and I think you seem to be one of the farthest along in implementing a quite complex TS-integrated LSP server, so any insights you have on these would be very appreciated! Feel free to move this discussion to a TS issue if you'd prefer. Thank you!

@jasonlyu123

Copy link
Copy Markdown
Member Author

The two mechanisms look good. We can probably check whether the requested document's version matches the version in the TS snapshot. Then wait for the "snapshotUpdate" event when our version is newer.

Speaking of document versions, I am also wondering whether unopened files (program files not opened in the editor) also need to have a version/hash? We might parse ts sourceFile's original Svelte content into svelte/html AST. I'm wondering if we can cache the AST result. Right now, it seems like we also cache the sourceFile and check if the new sourceFile is the same object. But I am not sure whether caching sourceFile leads to any memory leaks.

Client middleware

The problem with document-highlight is that VSCode only loads the first non-nullable result. Now that I've tried it again, it does allow multiple providers when I changed TypeScript's result to be null instead of an empty array. I probably did something wrong the last time I tried it. Is it possible for the TypeScript LSP to return null only for content-mapper files? ts/js files won't need that because VS Code will fall back to word highlight in that case. I'll open an issue in TypeScript about this.

Another place we might need a client middleware is completion. The problem is that we currently generate the default export as FileName__SvelteComponent_ instead of FileName to avoid name comflict with variables in the original code. This means auto-import will show the generated name. In the TS6 version, we removed the suffix in typescript-plugin and language-server. Find references are also affected by this suffix. Users will import the component with import FileName from '...' instead of the generated name. Therefore, the symbol name won't be the generated one. Because of that, find references will only show references from the same file where it's triggered.

In general, I think I will try to see if we can just let VS Code merge the result from two servers. So most likely we will need middleware in features that uses the "first not empty/null" result, or if we need to tweak the result.

@jasonlyu123

Copy link
Copy Markdown
Member Author

A few content mapper tasks after this PR is merged:

  • Update svelte-check --tsgo-experimental-api to use TypeScript 7.1 stable api.
    This blocks all the other tasks because of API changes.

LSP

  • Add content-mapper support for svelte-check --tsgo-experimental-api
  • Port component-references-related features for TypeScript 7
  • Add LSP middlewares
  • Diagnotics/notification message for missing content-mapper config

Drop svelte-check's dependency on TypeScript 6

  • Update svelte2tsx to use TypeScript 7 AST api
  • Remove top-level imports of TypeScript 6 in svelte-check and language server.

@xyrolle

xyrolle commented Oct 22, 2026

Copy link
Copy Markdown

I tested the content mapper from d3766bc on a small fixture with Svelte 5.57.2, TypeScript 6.0.3, and @typescript/native aliased to typescript@7.1.0-beta (Node 24.16.0, macOS arm64).

With the mapper configured in contentMappers, I ran:

node node_modules/@typescript/native/bin/tsc -p tsconfig.cm.json --noEmit --pretty false --runExternalCode

A .ts file importing an exported snippet from <script module> passes; without the mapper it reports TS2614, as in #3083.

After adding a string-to-number assignment error, the mapper reports TS2322 at Children1Col.svelte:3:11, matching svelte-check 4.7.7.

These are CLI fixture checks; I haven't tested editor integration.

@xyrolle

xyrolle commented Oct 22, 2026

Copy link
Copy Markdown

One additional CLI repro with d3766bc, using the same setup as above: Svelte 5.57.2, TypeScript 6.0.3, and @typescript/native aliased to typescript@7.1.0-beta.

src/Gen.svelte:

<script lang="ts" generics="const T extends readonly string[]">
    let { items }: { items: T } = $props();
</script>

<p>{items.length}</p>

With the mapper configured in contentMappers, running:

node node_modules/@typescript/native/bin/tsc -p tsconfig.cm.json --noEmit --pretty false --runExternalCode

reports:

src/Gen.svelte(28,46): error TS1277: 'const' modifier can only appear on a type parameter of a function, method or class

The diagnostic notes that this is a virtual-code location with no corresponding source location. The generated type alias keeps const:

type Gen__SvelteComponent_<const T extends readonly string[]> = InstanceType<typeof Gen__SvelteComponent_<T>>;

TypeScript doesn't allow const on a type alias's type parameters (6.0.3 and 7.1.0-beta both give TS1277 for type A<const T extends readonly string[]> = T, and nothing without const). svelte-check 4.7.7 reports no errors for the original component in default and --tsgo modes.

@xyrolle

xyrolle commented Oct 24, 2026 •

Copy link
Copy Markdown

Another CLI finding with d3766bc: with the mapper, TypeScript type-checks the .svelte sources a library ships next to their .svelte.d.ts, instead of using the declarations. On our app that was 124 errors inside layerchart 2.5.1 and bits-ui 2.19.5, which the default mode doesn't report.

Minimal repro, with the mapper from d3766bc installed as in the PR description, Svelte 5.57.2, TypeScript 6.0.3 and @typescript/native 7.1.0-beta (also 7.1.0-dev.20261022.1), Node 24.16.0, macOS arm64:

tsconfig.json:

{
  "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "noEmit": true, "skipLibCheck": true, "types": ["svelte"] },
  "include": ["src/**/*.ts", "src/**/*.svelte"]
}

tsconfig.cm.json:

{
  "extends": "./tsconfig.json",
  "contentMappers": [{ "package": "@sveltejs/ts-content-mapper", "extensions": [".svelte"] }]
}

node_modules/my-lib/package.json:

{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "exports": { ".": { "types": "./dist/index.d.ts", "svelte": "./dist/index.js" } }
}

node_modules/my-lib/dist/index.d.ts (and index.js): export { default as Button } from './Button.svelte';

node_modules/my-lib/dist/Button.svelte.d.ts:

declare const Button: import('svelte').Component<{ label: string }, {}, ''>;
type Button = ReturnType<typeof Button>;
export default Button;

node_modules/my-lib/dist/Button.svelte, with an error that only its source has:

<script lang="ts">
    let { label }: { label: string } = $props();
    const width: number = label;
</script>

<button style:width>{label}</button>

src/main.ts:

import { Button } from "my-lib";

export const button: typeof Button = Button;
node node_modules/@typescript/native/bin/tsc -p tsconfig.json --noEmit --pretty false
node node_modules/@typescript/native/bin/tsc -p tsconfig.cm.json --noEmit --pretty false --runExternalCode

The first passes. The second reports:

node_modules/my-lib/dist/Button.svelte(3,11): error TS2322: Type 'string' is not assignable to type 'number'.

svelte-check 4.7.7's default mode passes too.

The resolver takes a fully specified import of a mapped extension as the file itself, before any declaration file: resolver.go#L1758-L1764, with Button.svelte.d.ts only tried afterwards (#L1625-L1637). The language server prefers .svelte.d.ts on purpose (svelte-sys.ts#L20-L23, #2481). Is this something to raise with TypeScript, or is there a way for the mapper to prefer the declaration?

I tried the resolver change this would need, on microsoft/TypeScript 98add1bbed (the 22 Oct nightly's commit), in tsc/internal/module/resolver.go: a mapped file's declarations (X.d.svelte.ts, then X.svelte.d.ts) are tried before the mapped X.svelte.

Diff (+13/-6)
diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go
index 4b59f0582f..5a097b20d1 100644
--- a/tsc/internal/module/resolver.go
+++ b/tsc/internal/module/resolver.go
@@ -1756,15 +1756,22 @@ func (r *resolutionState) tryAddingExtensions(extensionless tspath.FileNameStem,
 		}
 		return continueSearching()
 	default:
-		if slices.Contains(r.resolver.extraExtensions, originalExtension) {
-			// A fully specified import of an extraExtension resolves directly to the file.
-			if resolved := r.tryExtension(originalExtension, extensionless, false); !resolved.shouldContinueSearching() {
-				resolved.resolvedUsingExtraExtensions = true
+		if extensions&extensionsDeclaration != 0 && !extensionless.AppendSuffix(originalExtension).IsDeclarationFile() {
+			if resolved := r.tryExtension(".d"+originalExtension+".ts", extensionless, false); !resolved.shouldContinueSearching() {
 				return resolved
 			}
 		}
-		if extensions&extensionsDeclaration != 0 && !extensionless.AppendSuffix(originalExtension).IsDeclarationFile() {
-			if resolved := r.tryExtension(".d"+originalExtension+".ts", extensionless, false); !resolved.shouldContinueSearching() {
+		if slices.Contains(r.resolver.extraExtensions, originalExtension) {
+			// A declaration file beside a mapped file describes it, as foo.d.ts describes foo.js:
+			// packages ship the mapped source their bundlers need next to its declarations.
+			if extensions&extensionsDeclaration != 0 {
+				if resolved := r.tryExtension(tspath.ExtensionDts, extensionless.AppendSuffix(originalExtension).AsStem(), false); !resolved.shouldContinueSearching() {
+					return resolved
+				}
+			}
+			// A fully specified import of an extraExtension resolves directly to the file.
+			if resolved := r.tryExtension(originalExtension, extensionless, false); !resolved.shouldContinueSearching() {
+				resolved.resolvedUsingExtraExtensions = true
 				return resolved
 			}
 		}

Built with npx hereby build. On our app, with this PR's mapper and tsc --runExternalCode, the npm nightly (7.1.0-dev.20261024.1) and the unpatched build both report 166 errors. The patched build removes exactly the 124 library errors (123 in layerchart, 1 in bits-ui), adds none, and still reports the deliberate errors we keep in one of the app's .svelte files and one .ts file. The 42 left are this PR's known gaps (SvelteKit auto-typing, and the const generics TS1277 above). It also fixes the repro above, and an app component without a declaration is still checked through the mapper.

Whether mapped files should prefer their declarations everywhere, as .js files do, or only in node_modules is the TypeScript team's call; this patch does it everywhere.

@jasonlyu123

Copy link
Copy Markdown
Member Author

In content mapper mode, TypeScript takes full control of module resolution logic. You can open an issue in TypeScript. That's indeed kind of a surprising result.

For the const modifier problem. It's not related to content mapper. It's just hidden in the current svelte-check because it can't be mapped to the source file. I think the const modifier can be removed from the generated type alias. But that would be a separate PR.

@jasonlyu123

jasonlyu123 commented Oct 24, 2026 •

Copy link
Copy Markdown
Member Author

For the module resolution problem. Now that I think of it. I guess that means that content mapper files are treated like a .ts file. It takes priority over declaration files and was not expected to be in the published libary. Svelte is the uncommon one that needs Svelte files to be shipped in component libraries.

Although not ideal, it's also not a complete blocker. Component libraries can still rely on the package.json export-map to remap the typing to .d.svelte.ts or .svelte.d.ts. The declaration files emitted by TypeScript 7.1 in content-mapper mode are .d.svelte.ts. And .svelte.d.ts might not work in NodeNext. Even if the priority is swept, a lot of the libraries might also need to migrate. Or remap it through an export map.

@jasonlyu123

Copy link
Copy Markdown
Member Author

Ah. Wait. It's still a problem. Export map won't work for barrel export.

@xyrolle

xyrolle commented Oct 24, 2026

Copy link
Copy Markdown

We tried a few ways around the barrel case (7.1 nightly with the mapper at this PR's head, bundler and NodeNext):

  • In the mapper: when a file under node_modules has a .svelte.d.ts beside it, return a re-export of that declaration instead of the transformed source. It fixes barrels and deep imports today without a TypeScript change, and matches what svelte-sys.ts does in the current mode. Open points: the transform cache doesn't see the .d.ts, and go-to-definition through the re-export.
  • In @sveltejs/package: emit ./X.svelte.js instead of ./X.svelte in the generated .d.ts. TypeScript's .js → .d.ts lookup then finds the declaration with or without the mapper, and it also fixes NodeNext with plain tsc today. It only helps packages once they republish.
  • In TypeScript: preferring declarations for mapped files inside node_modules fixes both, but a .ts file already beats its .d.ts there, so that's likely a harder sell.

Happy to share any of the patches if one is useful.

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.

4 participants