Skip to content

Commit ab81ce0

Browse files
committed
(WIP) package JSON exports support in TS
1 parent 2f390ac commit ab81ce0

File tree

2 files changed

+42
-12
lines changed

2 files changed

+42
-12
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ export function createSvelteModuleLoader(
299299
// without an exports map, because that may break too many existing projects.
300300
if (
301301
resolvedModule.isExternalLibraryImport &&
302+
// TODO check what happens if this resolves to a real .d.svelte.ts file
302303
resolvedModule.extension === '.d.svelte.ts' && // this tells us it's from an exports map
303304
snapshot.scriptKind !== ts.ScriptKind.TS
304305
) {

packages/typescript-plugin/src/module-loader.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import { ensureRealSvelteFilePath, isSvelteFilePath, isVirtualSvelteFilePath } f
1111
class ModuleResolutionCache {
1212
constructor(private readonly projectService: ts.server.ProjectService) {}
1313

14-
private cache = new Map<string, ts.ResolvedModuleFull>();
14+
private cache = new Map<string, ts.ResolvedModuleFull | null>();
1515

1616
/**
1717
* Tries to get a cached module.
1818
*/
19-
get(moduleName: string, containingFile: string): ts.ResolvedModuleFull | undefined {
19+
get(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null | undefined {
2020
return this.cache.get(this.getKey(moduleName, containingFile));
2121
}
2222

@@ -28,10 +28,14 @@ class ModuleResolutionCache {
2828
containingFile: string,
2929
resolvedModule: ts.ResolvedModuleFull | undefined
3030
) {
31-
if (!resolvedModule) {
31+
if (!resolvedModule && moduleName[0] === '.') {
32+
// We cache unresolved modules for non-relative imports, too, because it's very likely that they don't change
33+
// and we don't want to resolve them every time. If they do change, the original resolution mode will notice
34+
// most of the time, and the only time this would result in a stale cache entry is if a node_modules package
35+
// is added with a "svelte" condition and no "types" condition, which is rare enough.
3236
return;
3337
}
34-
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
38+
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule ?? null);
3539
}
3640

3741
/**
@@ -42,6 +46,7 @@ class ModuleResolutionCache {
4246
resolvedModuleName = this.projectService.toCanonicalFileName(resolvedModuleName);
4347
this.cache.forEach((val, key) => {
4448
if (
49+
val &&
4550
this.projectService.toCanonicalFileName(val.resolvedFileName) === resolvedModuleName
4651
) {
4752
this.cache.delete(key);
@@ -87,6 +92,8 @@ export function patchModuleLoader(
8792
if (lsHost.resolveModuleNameLiterals) {
8893
lsHost.resolveModuleNameLiterals = resolveModuleNameLiterals;
8994
} else {
95+
// TODO do we need to keep this around? We're requiring 5.0 now, so TS doesn't need it,
96+
// but would this break when other TS plugins are used and we no longer provide it?
9097
lsHost.resolveModuleNames = resolveModuleNames;
9198
}
9299

@@ -139,7 +146,9 @@ export function patchModuleLoader(
139146
return resolved.map((tsResolvedModule, idx) => {
140147
const moduleName = moduleNames[idx];
141148
if (
142-
!isSvelteFilePath(moduleName) ||
149+
// Only recheck relative Svelte imports or unresolved non-relative paths (which hint at node_modules,
150+
// where an exports map with "svelte" but not "types" could be present)
151+
(!isSvelteFilePath(moduleName) && (moduleName[0] === '.' || tsResolvedModule)) ||
143152
// corresponding .d.ts files take precedence over .svelte files
144153
tsResolvedModule?.resolvedFileName.endsWith('.d.ts') ||
145154
tsResolvedModule?.resolvedFileName.endsWith('.d.svelte.ts')
@@ -165,7 +174,8 @@ export function patchModuleLoader(
165174
const svelteResolvedModule = typescript.resolveModuleName(
166175
name,
167176
containingFile,
168-
compilerOptions,
177+
// customConditions makes the TS algorithm look at the "svelte" condition in exports maps
178+
{ ...compilerOptions, customConditions: ['svelte'] },
169179
svelteSys
170180
// don't set mode or else .svelte imports couldn't be resolved
171181
).resolvedModule;
@@ -225,19 +235,22 @@ export function patchModuleLoader(
225235

226236
return resolved.map((tsResolvedModule, idx) => {
227237
const moduleName = moduleLiterals[idx].text;
238+
const resolvedModule = tsResolvedModule.resolvedModule;
228239

229240
if (
230-
!isSvelteFilePath(moduleName) ||
241+
// Only recheck relative Svelte imports or unresolved non-relative paths (which hint at node_modules,
242+
// where an exports map with "svelte" but not "types" could be present)
243+
(!isSvelteFilePath(moduleName) && (moduleName[0] === '.' || resolvedModule)) ||
231244
// corresponding .d.ts files take precedence over .svelte files
232-
tsResolvedModule?.resolvedModule?.resolvedFileName.endsWith('.d.ts') ||
233-
tsResolvedModule?.resolvedModule?.resolvedFileName.endsWith('.d.svelte.ts')
245+
resolvedModule?.resolvedFileName.endsWith('.d.ts') ||
246+
resolvedModule?.resolvedFileName.endsWith('.d.svelte.ts')
234247
) {
235248
return tsResolvedModule;
236249
}
237250

238251
const result = resolveSvelteModuleNameFromCache(moduleName, containingFile, options);
239252
// .svelte takes precedence over .svelte.ts etc
240-
return result ?? tsResolvedModule;
253+
return result.resolvedModule ? result : tsResolvedModule;
241254
});
242255
}
243256

@@ -247,13 +260,29 @@ export function patchModuleLoader(
247260
options: ts.CompilerOptions
248261
) {
249262
const cachedModule = moduleCache.get(moduleName, containingFile);
250-
if (cachedModule) {
263+
if (typeof cachedModule === 'object') {
251264
return {
252-
resolvedModule: cachedModule
265+
resolvedModule: cachedModule ?? undefined
253266
};
254267
}
255268

256269
const resolvedModule = resolveSvelteModuleName(moduleName, containingFile, options);
270+
271+
// Align with TypeScript behavior: If the Svelte file is not using TypeScript,
272+
// mark it as unresolved so that people need to provide a .d.ts file.
273+
// For backwards compatibility we're not doing this for files from packages
274+
// without an exports map, because that may break too many existing projects.
275+
if (
276+
resolvedModule?.isExternalLibraryImport && // TODO how to check this is not from a non-exports map?
277+
// TODO check what happens if this resolves to a real .d.svelte.ts file
278+
resolvedModule.extension === '.ts' // this tells us it's from an exports map
279+
) {
280+
moduleCache.set(moduleName, containingFile, undefined);
281+
return {
282+
resolvedModule: undefined
283+
};
284+
}
285+
257286
moduleCache.set(moduleName, containingFile, resolvedModule);
258287
return {
259288
resolvedModule: resolvedModule

0 commit comments

Comments
 (0)