Skip to content

Commit

Permalink
feat: resolve Svelte components using TS from exports map
Browse files Browse the repository at this point in the history
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
  • Loading branch information
dummdidumm committed Aug 28, 2024
1 parent 8c080cf commit 1f8b5a0
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 2 deletions.
20 changes: 19 additions & 1 deletion packages/language-server/src/plugins/typescript/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,13 @@ class ImpliedNodeFormatResolver {
return undefined;
}

let mode = undefined;
let mode: ReturnType<typeof ts.getModeForResolutionAtIndex> = undefined;
if (sourceFile) {
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions);
if (!mode && isSvelteFilePath(importPath)) {
mode = ts.ModuleKind.ESNext; // necessary for TS' module resolution to go into the right branches
}
}
return mode;
}
Expand Down Expand Up @@ -293,6 +296,21 @@ export function createSvelteModuleLoader(

const snapshot = getSnapshot(resolvedFileName);

// Align with TypeScript behavior: If the Svelte file is not using TypeScript,
// mark it as unresolved so that people need to provide a .d.ts file.
// For backwards compatibility we're not doing this for files from packages
// without an exports map, because that may break too many existing projects.
if (
resolvedModule.isExternalLibraryImport &&
resolvedModule.extension === '.d.svelte.ts' && // this tells us it's from an exports map
snapshot.scriptKind !== ts.ScriptKind.TS
) {
return {
...resolvedModuleWithFailedLookup,
resolvedModule: undefined
};
}

const resolvedSvelteModule: ts.ResolvedModuleFull = {
extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind),
resolvedFileName,
Expand Down
7 changes: 7 additions & 0 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,13 @@ async function createLanguageService(
}
}

// Necessary to be able to resolve export maps that only contain a "svelte" condition without an accompanying "types" condition
// https://www.typescriptlang.org/tsconfig/#customConditions
if (!compilerOptions.customConditions?.includes('svelte')) {
compilerOptions.customConditions = compilerOptions.customConditions ?? [];
compilerOptions.customConditions.push('svelte');
}

const svelteConfigDiagnostics = checkSvelteInput(parsedConfig);
if (svelteConfigDiagnostics.length > 0) {
docContext.reportConfigError?.({
Expand Down
8 changes: 7 additions & 1 deletion packages/language-server/src/plugins/typescript/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ export function isVirtualSvelteFilePath(filePath: string) {
}

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

export function toVirtualSvelteFilePath(filePath: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{
"code": 2307,
"message": "Cannot find module 'package/y' or its corresponding type declarations.",
"range": {
"start": {
"character": 38,
"line": 3
},
"end": {
"character": 49,
"line": 3
}
},
"severity": 1,
"source": "ts",
"tags": []
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import DefaultSvelteWithTS from 'package';
import SubWithDTS from 'package/x';
import SubWithoutDTSAndNotTS from 'package/y';
</script>

<DefaultSvelteWithTS />
<SubWithDTS />
<SubWithoutDTSAndNotTS />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"moduleResolution": "Bundler"
}
}

0 comments on commit 1f8b5a0

Please sign in to comment.