-
-
Notifications
You must be signed in to change notification settings - Fork 233
Expand file tree
/
Copy pathmodule-loader.ts
More file actions
291 lines (263 loc) · 11.8 KB
/
module-loader.ts
File metadata and controls
291 lines (263 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import type ts from 'typescript/lib/tsserverlibrary';
import { ConfigManager } from './config-manager';
import { Logger } from './logger';
import { SvelteSnapshotManager } from './svelte-snapshots';
import { createSvelteSys } from './svelte-sys';
import { ensureRealSvelteFilePath, isSvelteFilePath, isVirtualSvelteFilePath } from './utils';
/**
* Caches resolved modules.
*/
class ModuleResolutionCache {
constructor(private readonly projectService: ts.server.ProjectService) {}
private cache = new Map<string, ts.ResolvedModuleFull | null>();
/**
* Tries to get a cached module.
*/
get(moduleName: string, containingFile: string): ts.ResolvedModuleFull | null | undefined {
return this.cache.get(this.getKey(moduleName, containingFile));
}
/**
* Caches resolved module, if it is not undefined.
*/
set(
moduleName: string,
containingFile: string,
resolvedModule: ts.ResolvedModuleFull | undefined
) {
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 ?? null);
}
/**
* Deletes module from cache. Call this if a file was deleted.
* @param resolvedModuleName full path of the module
*/
delete(resolvedModuleName: string): void {
resolvedModuleName = this.projectService.toCanonicalFileName(resolvedModuleName);
this.cache.forEach((val, key) => {
if (
val &&
this.projectService.toCanonicalFileName(val.resolvedFileName) === resolvedModuleName
) {
this.cache.delete(key);
}
});
}
clear() {
this.cache.clear();
}
private getKey(moduleName: string, containingFile: string) {
return (
this.projectService.toCanonicalFileName(containingFile) +
':::' +
this.projectService.toCanonicalFileName(ensureRealSvelteFilePath(moduleName))
);
}
}
/**
* Creates a module loader than can also resolve `.svelte` files.
*
* The typescript language service tries to look up other files that are referenced in the currently open svelte file.
* For `.ts`/`.js` files this works, for `.svelte` files it does not by default.
* Reason: The typescript language service does not know about the `.svelte` file ending,
* so it assumes it's a normal typescript file and searches for files like `../Component.svelte.ts`, which is wrong.
* In order to fix this, we need to wrap typescript's module resolution and reroute all `.svelte.ts` file lookups to .svelte.
*/
export function patchModuleLoader(
logger: Logger,
snapshotManager: SvelteSnapshotManager,
typescript: typeof ts,
lsHost: ts.LanguageServiceHost,
project: ts.server.Project,
configManager: ConfigManager
): { dispose: () => void } {
const svelteSys = createSvelteSys(typescript, logger);
const moduleCache = new ModuleResolutionCache(project.projectService);
const origResolveModuleNames = lsHost.resolveModuleNames?.bind(lsHost);
const origResolveModuleNamLiterals = lsHost.resolveModuleNameLiterals?.bind(lsHost);
if (lsHost.resolveModuleNameLiterals) {
lsHost.resolveModuleNameLiterals = resolveModuleNameLiterals;
} else {
// TODO do we need to keep this around? We're requiring 5.0 now, so TS doesn't need it,
// but would this break when other TS plugins are used and we no longer provide it?
lsHost.resolveModuleNames = resolveModuleNames;
}
const origRemoveFile = project.removeFile.bind(project);
project.removeFile = (info, fileExists, detachFromProject) => {
logger.log('File is being removed. Delete from cache: ', info.fileName);
moduleCache.delete(info.fileName);
return origRemoveFile(info, fileExists, detachFromProject);
};
const onConfigChanged = () => {
moduleCache.clear();
};
configManager.onConfigurationChanged(onConfigChanged);
return {
dispose() {
configManager.removeConfigurationChangeListener(onConfigChanged);
moduleCache.clear();
}
};
function resolveModuleNames(
moduleNames: string[],
containingFile: string,
reusedNames: string[] | undefined,
redirectedReference: ts.ResolvedProjectReference | undefined,
compilerOptions: ts.CompilerOptions,
containingSourceFile?: ts.SourceFile
): Array<ts.ResolvedModule | undefined> {
logger.debug('Resolving modules names for ' + containingFile);
// Try resolving all module names with the original method first.
// The ones that are undefined will be re-checked if they are a
// Svelte file and if so, are resolved, too. This way we can defer
// all module resolving logic except for Svelte files to TypeScript.
const resolved =
origResolveModuleNames?.(
moduleNames,
containingFile,
reusedNames,
redirectedReference,
compilerOptions,
containingSourceFile
) || Array.from<undefined>(Array(moduleNames.length));
if (!configManager.getConfig().enable) {
return resolved;
}
return resolved.map((tsResolvedModule, idx) => {
const moduleName = moduleNames[idx];
if (
// 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')
) {
return tsResolvedModule;
}
const result = resolveSvelteModuleNameFromCache(
moduleName,
containingFile,
compilerOptions
).resolvedModule;
// .svelte takes precedence over .svelte.ts etc
return result ?? tsResolvedModule;
});
}
function resolveSvelteModuleName(
name: string,
containingFile: string,
compilerOptions: ts.CompilerOptions
): ts.ResolvedModuleFull | undefined {
const svelteResolvedModule = typescript.resolveModuleName(
name,
containingFile,
// 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;
if (
!svelteResolvedModule ||
!isVirtualSvelteFilePath(svelteResolvedModule.resolvedFileName)
) {
return svelteResolvedModule;
}
const resolvedFileName = ensureRealSvelteFilePath(svelteResolvedModule.resolvedFileName);
logger.log('Resolved', name, 'to Svelte file', resolvedFileName);
const snapshot = snapshotManager.create(resolvedFileName);
if (!snapshot) {
return undefined;
}
const resolvedSvelteModule: ts.ResolvedModuleFull = {
extension: snapshot.isTsFile ? typescript.Extension.Ts : typescript.Extension.Js,
resolvedFileName,
isExternalLibraryImport: svelteResolvedModule.isExternalLibraryImport
};
return resolvedSvelteModule;
}
function resolveModuleNameLiterals(
moduleLiterals: readonly ts.StringLiteralLike[],
containingFile: string,
redirectedReference: ts.ResolvedProjectReference | undefined,
options: ts.CompilerOptions,
containingSourceFile: ts.SourceFile,
reusedNames: readonly ts.StringLiteralLike[] | undefined
): readonly ts.ResolvedModuleWithFailedLookupLocations[] {
logger.debug('Resolving modules names for ' + containingFile);
// Try resolving all module names with the original method first.
// The ones that are undefined will be re-checked if they are a
// Svelte file and if so, are resolved, too. This way we can defer
// all module resolving logic except for Svelte files to TypeScript.
const resolved =
origResolveModuleNamLiterals?.(
moduleLiterals,
containingFile,
redirectedReference,
options,
containingSourceFile,
reusedNames
) ??
moduleLiterals.map(
(): ts.ResolvedModuleWithFailedLookupLocations => ({
resolvedModule: undefined
})
);
if (!configManager.getConfig().enable) {
return resolved;
}
return resolved.map((tsResolvedModule, idx) => {
const moduleName = moduleLiterals[idx].text;
const resolvedModule = tsResolvedModule.resolvedModule;
if (
// 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')
) {
return tsResolvedModule;
}
const result = resolveSvelteModuleNameFromCache(moduleName, containingFile, options);
// .svelte takes precedence over .svelte.ts etc
return result.resolvedModule ? result : tsResolvedModule;
});
}
function resolveSvelteModuleNameFromCache(
moduleName: string,
containingFile: string,
options: ts.CompilerOptions
) {
const cachedModule = moduleCache.get(moduleName, containingFile);
if (typeof cachedModule === 'object') {
return {
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
};
}
}