Skip to content

Commit 1f8b5a0

Browse files
committed
feat: resolve Svelte components using TS from exports map
This change allows people to write export maps using only a `svelte` condition (and no `types` condition) and still have the types for their components resolved (i.e. the import is found) as long as they use TypeScript (i.e. have lang="ts" attribute) inside it. This should help people using monorepo setups with strong typings and not wanting to provide d.ts files alongside. This is achieved doing three adjustments: - add `customConditions: ['svelte']` to the compiler options, so that TypeScript's resolution algorithm takes it into account - ensure that Svelte files have a module kind of ESM, so that TypeScript's resolution algorithm goes into the right branches - deal with `.d.svelte.ts` files in the context of an exports map, because that's what TypeScript will try to resolve this to in the end This is also related to #1056 insofar that we align with TypeScript for this new capability: We don't resolve the file if it's a component not using TypeScript (i.e. not having the lang="ts" tag), similar to how TypeScript does not resolve .js files within node_modules
1 parent 8c080cf commit 1f8b5a0

File tree

6 files changed

+68
-2
lines changed

6 files changed

+68
-2
lines changed

packages/language-server/src/plugins/typescript/module-loader.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,13 @@ class ImpliedNodeFormatResolver {
104104
return undefined;
105105
}
106106

107-
let mode = undefined;
107+
let mode: ReturnType<typeof ts.getModeForResolutionAtIndex> = undefined;
108108
if (sourceFile) {
109109
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
110110
mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions);
111+
if (!mode && isSvelteFilePath(importPath)) {
112+
mode = ts.ModuleKind.ESNext; // necessary for TS' module resolution to go into the right branches
113+
}
111114
}
112115
return mode;
113116
}
@@ -293,6 +296,21 @@ export function createSvelteModuleLoader(
293296

294297
const snapshot = getSnapshot(resolvedFileName);
295298

299+
// Align with TypeScript behavior: If the Svelte file is not using TypeScript,
300+
// mark it as unresolved so that people need to provide a .d.ts file.
301+
// For backwards compatibility we're not doing this for files from packages
302+
// without an exports map, because that may break too many existing projects.
303+
if (
304+
resolvedModule.isExternalLibraryImport &&
305+
resolvedModule.extension === '.d.svelte.ts' && // this tells us it's from an exports map
306+
snapshot.scriptKind !== ts.ScriptKind.TS
307+
) {
308+
return {
309+
...resolvedModuleWithFailedLookup,
310+
resolvedModule: undefined
311+
};
312+
}
313+
296314
const resolvedSvelteModule: ts.ResolvedModuleFull = {
297315
extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind),
298316
resolvedFileName,

packages/language-server/src/plugins/typescript/service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,13 @@ async function createLanguageService(
736736
}
737737
}
738738

739+
// Necessary to be able to resolve export maps that only contain a "svelte" condition without an accompanying "types" condition
740+
// https://www.typescriptlang.org/tsconfig/#customConditions
741+
if (!compilerOptions.customConditions?.includes('svelte')) {
742+
compilerOptions.customConditions = compilerOptions.customConditions ?? [];
743+
compilerOptions.customConditions.push('svelte');
744+
}
745+
739746
const svelteConfigDiagnostics = checkSvelteInput(parsedConfig);
740747
if (svelteConfigDiagnostics.length > 0) {
741748
docContext.reportConfigError?.({

packages/language-server/src/plugins/typescript/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,13 @@ export function isVirtualSvelteFilePath(filePath: string) {
7474
}
7575

7676
export function toRealSvelteFilePath(filePath: string) {
77-
return filePath.slice(0, -'.ts'.length);
77+
filePath = filePath.slice(0, -'.ts'.length);
78+
// When a .svelte file referenced inside an exports map of a package.json is tried to be resolved,
79+
// TypeScript will probe for the file with a .d.svelte.ts extension.
80+
if (filePath.endsWith('.d.svelte')) {
81+
filePath = filePath.slice(0, -8) + 'svelte';
82+
}
83+
return filePath;
7884
}
7985

8086
export function toVirtualSvelteFilePath(filePath: string) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[
2+
{
3+
"code": 2307,
4+
"message": "Cannot find module 'package/y' or its corresponding type declarations.",
5+
"range": {
6+
"start": {
7+
"character": 38,
8+
"line": 3
9+
},
10+
"end": {
11+
"character": 49,
12+
"line": 3
13+
}
14+
},
15+
"severity": 1,
16+
"source": "ts",
17+
"tags": []
18+
}
19+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import DefaultSvelteWithTS from 'package';
3+
import SubWithDTS from 'package/x';
4+
import SubWithoutDTSAndNotTS from 'package/y';
5+
</script>
6+
7+
<DefaultSvelteWithTS />
8+
<SubWithDTS />
9+
<SubWithoutDTSAndNotTS />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"module": "esnext",
4+
"target": "esnext",
5+
"moduleResolution": "Bundler"
6+
}
7+
}

0 commit comments

Comments
 (0)