diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 4cbde00b8..86080dc15 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -293,6 +293,22 @@ 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 && + // TODO check what happens if this resolves to a real .d.svelte.ts file + 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, diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 8f5720bb0..f94e8e6ff 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -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?.({ diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/expectedv2.json index 58680d099..001dff42d 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/expectedv2.json +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/expectedv2.json @@ -1,21 +1,4 @@ [ - { - "code": 2307, - "message": "Cannot find module 'package' or its corresponding type declarations.", - "range": { - "end": { - "character": 45, - "line": 1 - }, - "start": { - "character": 36, - "line": 1 - } - }, - "severity": 1, - "source": "ts", - "tags": [] - }, { "code": 2307, "message": "Cannot find module 'package/y' or its corresponding type declarations.", diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/input.svelte index 2775a7582..3a6782e1b 100644 --- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/input.svelte +++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/input.svelte @@ -1,5 +1,5 @@ diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 50c793c6f..266496dc7 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -75,7 +75,8 @@ describe('service', () => { strict: true, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Node10, - target: ts.ScriptTarget.ESNext + target: ts.ScriptTarget.ESNext, + customConditions: ['svelte'] }); }); @@ -185,7 +186,8 @@ describe('service', () => { moduleResolution: ts.ModuleResolutionKind.Node10, noEmit: true, skipLibCheck: true, - target: ts.ScriptTarget.ESNext + target: ts.ScriptTarget.ESNext, + customConditions: ['svelte'] }); }); diff --git a/packages/typescript-plugin/src/module-loader.ts b/packages/typescript-plugin/src/module-loader.ts index 569f073b3..9f5e6f93c 100644 --- a/packages/typescript-plugin/src/module-loader.ts +++ b/packages/typescript-plugin/src/module-loader.ts @@ -11,12 +11,12 @@ import { ensureRealSvelteFilePath, isSvelteFilePath, isVirtualSvelteFilePath } f class ModuleResolutionCache { constructor(private readonly projectService: ts.server.ProjectService) {} - private cache = new Map(); + private cache = new Map(); /** * Tries to get a cached module. */ - get(moduleName: string, containingFile: string): ts.ResolvedModuleFull | undefined { + get(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null | undefined { return this.cache.get(this.getKey(moduleName, containingFile)); } @@ -28,10 +28,14 @@ class ModuleResolutionCache { containingFile: string, resolvedModule: ts.ResolvedModuleFull | undefined ) { - if (!resolvedModule) { + if (!resolvedModule && moduleName[0] === '.') { + // We cache unresolved modules for non-relative imports, too, because it's very likely that they don't change + // and we don't want to resolve them every time. If they do change, the original resolution mode will notice + // most of the time, and the only time this would result in a stale cache entry is if a node_modules package + // is added with a "svelte" condition and no "types" condition, which is rare enough. return; } - this.cache.set(this.getKey(moduleName, containingFile), resolvedModule); + this.cache.set(this.getKey(moduleName, containingFile), resolvedModule ?? null); } /** @@ -42,6 +46,7 @@ class ModuleResolutionCache { resolvedModuleName = this.projectService.toCanonicalFileName(resolvedModuleName); this.cache.forEach((val, key) => { if ( + val && this.projectService.toCanonicalFileName(val.resolvedFileName) === resolvedModuleName ) { this.cache.delete(key); @@ -141,7 +146,9 @@ export function patchModuleLoader( return resolved.map((tsResolvedModule, idx) => { const moduleName = moduleNames[idx]; if ( - !isSvelteFilePath(moduleName) || + // Only recheck relative Svelte imports or unresolved non-relative paths (which hint at node_modules + // where an exports map with "svelte" but not "types" could be present) + (!isSvelteFilePath(moduleName) && (moduleName[0] === '.' || tsResolvedModule)) || // corresponding .d.ts files take precedence over .svelte files tsResolvedModule?.resolvedFileName.endsWith('.d.ts') || tsResolvedModule?.resolvedFileName.endsWith('.d.svelte.ts') @@ -167,7 +174,8 @@ export function patchModuleLoader( const svelteResolvedModule = typescript.resolveModuleName( name, containingFile, - compilerOptions, + // customConditions makes the TS algorithm look at the "svelte" condition in exports maps + { ...compilerOptions, customConditions: ['svelte'] }, svelteSys // don't set mode or else .svelte imports couldn't be resolved ).resolvedModule; @@ -230,7 +238,9 @@ export function patchModuleLoader( const resolvedModule = tsResolvedModule.resolvedModule; if ( - !isSvelteFilePath(moduleName) || + // Only recheck relative Svelte imports or unresolved non-relative paths (which hint at node_modules, + // where an exports map with "svelte" but not "types" could be present) + (!isSvelteFilePath(moduleName) && (moduleName[0] === '.' || resolvedModule)) || // corresponding .d.ts files take precedence over .svelte files resolvedModule?.resolvedFileName.endsWith('.d.ts') || resolvedModule?.resolvedFileName.endsWith('.d.svelte.ts') @@ -250,14 +260,29 @@ export function patchModuleLoader( options: ts.CompilerOptions ) { const cachedModule = moduleCache.get(moduleName, containingFile); - if (cachedModule) { + if (typeof cachedModule === 'object') { return { - resolvedModule: cachedModule + resolvedModule: cachedModule ?? undefined }; } const resolvedModule = resolveSvelteModuleName(moduleName, containingFile, options); + // 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 && // TODO how to check this is not from a non-exports map? + // TODO check what happens if this resolves to a real .d.svelte.ts file + resolvedModule.extension === '.ts' // this tells us it's from an exports map + ) { + moduleCache.set(moduleName, containingFile, undefined); + return { + resolvedModule: undefined + }; + } + moduleCache.set(moduleName, containingFile, resolvedModule); return { resolvedModule: resolvedModule