Skip to content

Commit 8e530a8

Browse files
author
Simon Holthausen
committed
(feat) resolve svelte module names
makes it possible to navigate from one svelte file to the other by clicking on its uri.
1 parent 442d2b2 commit 8e530a8

File tree

9 files changed

+378
-18
lines changed

9 files changed

+378
-18
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,6 @@ typings/
6565

6666
# tdsx
6767
dist
68+
69+
# VSCode history extension
70+
.history

.vscode/launch.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"stopOnEntry": false,
1111
"sourceMaps": true,
1212
"outFiles": ["${workspaceRoot}/packkages/svelte-vscode/dist/**/*.js"],
13-
"preLaunchTask": "npm: watch"
13+
"preLaunchTask": "npm: watch"
1414
},
1515
{
1616
"type": "node",
@@ -23,6 +23,9 @@
2323
"--colors",
2424
"${workspaceFolder}/packages/*/test/**/*.ts"
2525
],
26+
"env": {
27+
"TS_NODE_COMPILER_OPTIONS": "{\"esModuleInterop\":true}"
28+
},
2629
"console": "integratedTerminal",
2730
"internalConsoleOptions": "neverOpen"
2831
},

packages/language-server/src/api/wrapFragmentPlugin.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ export function wrapFragmentPlugin<P>(plugin: P, fragmentPredicate: FragmentPred
6161
});
6262
},
6363

64-
openDocument: (document: TextDocumentItem) => host.openDocument(document),
64+
openDocument: (document: TextDocumentItem) => {
65+
return getFragment(host.openDocument(document))! as Document;
66+
},
6567
lockDocument: (uri: string) => host.lockDocument(uri),
6668
getConfig: (key: string) => host.getConfig(key),
6769
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import ts from 'typescript';
2+
import { isVirtualSvelteFilePath, ensureRealSvelteFilePath, isSvelteFilePath } from './utils';
3+
import { isAbsolute } from 'path';
4+
import { DocumentSnapshot } from './DocumentSnapshot';
5+
import { createSvelteSys } from './svelte-sys';
6+
7+
/**
8+
* Caches resolved modules.
9+
*/
10+
class ModuleResolutionCache {
11+
private cache = new Map<string, ts.ResolvedModule>();
12+
13+
/**
14+
* Tries to get a cached module.
15+
*/
16+
get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined {
17+
return this.cache.get(this.getKey(moduleName, containingFile));
18+
}
19+
20+
/**
21+
* Caches resolved module, if it is not undefined.
22+
*/
23+
set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | undefined) {
24+
if (!resolvedModule) {
25+
return;
26+
}
27+
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
28+
}
29+
30+
private getKey(moduleName: string, containingFile: string) {
31+
return containingFile + ':::' + ensureRealSvelteFilePath(moduleName);
32+
}
33+
}
34+
35+
/**
36+
* Creates a module loader specifically for `.svelte` files.
37+
*
38+
* The typescript language service tries to look up other files that are referenced in the currently open svelte file.
39+
* For `.ts`/`.js` files this works, for `.svelte` files it does not by default.
40+
* Reason: The typescript language service does not know about the `.svelte` file ending,
41+
* so it assumes it's a normal typescript file and searches for files like `../Component.svelte.ts`, which is wrong.
42+
* In order to fix this, we need to wrap typescript's module resolution and reroute all `.svelte.ts` file lookups to .svelte.
43+
*
44+
* @param getSvelteSnapshot A function which returns a fully preprocessed typescript/javascript snapshot
45+
* @param compilerOptions The typescript compiler options
46+
*/
47+
export function createSvelteModuleLoader(
48+
getSvelteSnapshot: (fileName: string) => DocumentSnapshot | undefined,
49+
compilerOptions: ts.CompilerOptions,
50+
) {
51+
const svelteSys = createSvelteSys(getSvelteSnapshot);
52+
const moduleCache = new ModuleResolutionCache();
53+
54+
return {
55+
fileExists: svelteSys.fileExists,
56+
readFile: svelteSys.readFile,
57+
resolveModuleNames,
58+
};
59+
60+
function resolveModuleNames(
61+
moduleNames: string[],
62+
containingFile: string,
63+
): (ts.ResolvedModule | undefined)[] {
64+
return moduleNames.map(moduleName => {
65+
const cachedModule = moduleCache.get(moduleName, containingFile);
66+
if (cachedModule) {
67+
return cachedModule;
68+
}
69+
70+
const resolvedModule = resolveModuleName(moduleName, containingFile);
71+
moduleCache.set(moduleName, containingFile, resolvedModule);
72+
return resolvedModule;
73+
});
74+
}
75+
76+
function resolveModuleName(
77+
name: string,
78+
containingFile: string,
79+
): ts.ResolvedModule | undefined {
80+
// In the normal case, delegate to ts.resolveModuleName.
81+
// In the relative-imported.svelte case, delegate to our own svelte module loader.
82+
if (isAbsolute(name) || !isSvelteFilePath(name)) {
83+
return ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys)
84+
.resolvedModule;
85+
}
86+
87+
const tsResolvedModule = ts.resolveModuleName(
88+
name,
89+
containingFile,
90+
compilerOptions,
91+
svelteSys,
92+
).resolvedModule;
93+
if (!tsResolvedModule || !isVirtualSvelteFilePath(tsResolvedModule.resolvedFileName)) {
94+
return tsResolvedModule;
95+
}
96+
97+
const resolvedFileName = ensureRealSvelteFilePath(tsResolvedModule.resolvedFileName);
98+
const snapshot = getSvelteSnapshot(resolvedFileName);
99+
const extension: ts.Extension =
100+
snapshot && snapshot.scriptKind === ts.ScriptKind.TS
101+
? ts.Extension.Ts
102+
: ts.Extension.Js;
103+
104+
const resolvedSvelteModule: ts.ResolvedModuleFull = {
105+
extension,
106+
resolvedFileName,
107+
};
108+
return resolvedSvelteModule;
109+
}
110+
}

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

+15-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import ts from 'typescript';
2-
import { DocumentSnapshot } from './DocumentSnapshot';
3-
import { isSvelte, getScriptKindFromFileName } from './utils';
41
import { dirname, resolve } from 'path';
2+
import ts from 'typescript';
53
import { Document } from '../../api';
64
import { getSveltePackageInfo } from '../svelte/sveltePackage';
5+
import { DocumentSnapshot } from './DocumentSnapshot';
6+
import { createSvelteModuleLoader } from './module-loader';
7+
import { ensureRealSvelteFilePath, getScriptKindFromFileName, isSvelteFilePath } from './utils';
78

89
export interface LanguageServiceContainer {
910
getService(): ts.LanguageService;
@@ -49,9 +50,7 @@ export function createLanguageService(
4950
module: ts.ModuleKind.ESNext,
5051
moduleResolution: ts.ModuleResolutionKind.NodeJs,
5152
allowJs: true,
52-
types: [
53-
resolve(sveltePkgInfo.path, 'types', 'runtime')
54-
]
53+
types: [resolve(sveltePkgInfo.path, 'types', 'runtime')],
5554
};
5655

5756
const configJson = tsconfigPath && ts.readConfigFile(tsconfigPath, ts.sys.readFile).config;
@@ -70,6 +69,8 @@ export function createLanguageService(
7069
compilerOptions = { ...compilerOptions, ...parsedConfig.options };
7170
}
7271

72+
const svelteModuleLoader = createSvelteModuleLoader(getSvelteSnapshot, compilerOptions);
73+
7374
const host: ts.LanguageServiceHost = {
7475
getCompilationSettings: () => compilerOptions,
7576
getScriptFileNames: () => Array.from(new Set([...files, ...Array.from(documents.keys())])),
@@ -87,13 +88,13 @@ export function createLanguageService(
8788
},
8889
getCurrentDirectory: () => workspacePath,
8990
getDefaultLibFileName: ts.getDefaultLibFilePath,
90-
91-
fileExists: ts.sys.fileExists,
92-
readFile: ts.sys.readFile,
91+
fileExists: svelteModuleLoader.fileExists,
92+
readFile: svelteModuleLoader.readFile,
93+
resolveModuleNames: svelteModuleLoader.resolveModuleNames,
9394
readDirectory: ts.sys.readDirectory,
9495
getScriptKind: (fileName: string) => {
9596
const doc = getSvelteSnapshot(fileName);
96-
if(doc) {
97+
if (doc) {
9798
return doc.scriptKind;
9899
}
99100

@@ -121,15 +122,15 @@ export function createLanguageService(
121122
}
122123

123124
function getSvelteSnapshot(fileName: string): DocumentSnapshot | undefined {
125+
fileName = ensureRealSvelteFilePath(fileName);
124126
const doc = documents.get(fileName);
125127
if (doc) {
126128
return doc;
127129
}
128130

129-
if (isSvelte(fileName)) {
130-
const doc = DocumentSnapshot.fromDocument(
131-
createDocument(fileName, ts.sys.readFile(fileName) || ''),
132-
);
131+
if (isSvelteFilePath(fileName)) {
132+
const file = ts.sys.readFile(fileName) || '';
133+
const doc = DocumentSnapshot.fromDocument(createDocument(fileName, file));
133134
documents.set(fileName, doc);
134135
return doc;
135136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { DocumentSnapshot } from './DocumentSnapshot';
2+
import ts from 'typescript';
3+
import { ensureRealSvelteFilePath, isVirtualSvelteFilePath, toRealSvelteFilePath } from './utils';
4+
5+
/**
6+
* This should only be accessed by TS svelte module resolution.
7+
*/
8+
export function createSvelteSys(
9+
getSvelteSnapshot: (fileName: string) => DocumentSnapshot | undefined,
10+
) {
11+
const svelteSys: ts.System = {
12+
...ts.sys,
13+
fileExists(path: string) {
14+
return ts.sys.fileExists(ensureRealSvelteFilePath(path));
15+
},
16+
readFile(path, encoding) {
17+
if (isVirtualSvelteFilePath(path)) {
18+
const fileText = ts.sys.readFile(toRealSvelteFilePath(path), encoding);
19+
const snapshot = getSvelteSnapshot(fileText!);
20+
return fileText ? snapshot?.getText(0, snapshot.getLength()) : fileText;
21+
}
22+
const fileText = ts.sys.readFile(path, encoding);
23+
return fileText;
24+
},
25+
};
26+
27+
if (ts.sys.realpath) {
28+
const realpath = ts.sys.realpath;
29+
svelteSys.realpath = function(path) {
30+
if (isVirtualSvelteFilePath(path)) {
31+
return realpath(toRealSvelteFilePath(path)) + '.ts';
32+
}
33+
return realpath(path);
34+
};
35+
}
36+
37+
return svelteSys;
38+
}

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
1919
}
2020
}
2121

22-
export function getScriptKindFromAttributes(attrs: Record<string, string>): ts.ScriptKind {
22+
export function getScriptKindFromAttributes(
23+
attrs: Record<string, string>,
24+
): ts.ScriptKind.TS | ts.ScriptKind.JS {
2325
const type = attrs.lang || attrs.type;
2426

2527
switch (type) {
@@ -33,10 +35,22 @@ export function getScriptKindFromAttributes(attrs: Record<string, string>): ts.S
3335
}
3436
}
3537

36-
export function isSvelte(filePath: string) {
38+
export function isSvelteFilePath(filePath: string) {
3739
return filePath.endsWith('.svelte');
3840
}
3941

42+
export function isVirtualSvelteFilePath(filePath: string) {
43+
return filePath.endsWith('.svelte.ts');
44+
}
45+
46+
export function toRealSvelteFilePath(filePath: string) {
47+
return filePath.slice(0, -'.ts'.length);
48+
}
49+
50+
export function ensureRealSvelteFilePath(filePath: string) {
51+
return isVirtualSvelteFilePath(filePath) ? toRealSvelteFilePath(filePath) : filePath;
52+
}
53+
4054
export function convertRange(
4155
document: Document,
4256
range: { start?: number; length?: number },

0 commit comments

Comments
 (0)