Skip to content

Commit a3a7a91

Browse files
authored
fix: Svelte module resolution tweaks (#2481)
Svelte files are now resolved by intercepting checks for .d.svelte.ts instead of .svelte.ts. As a consequence, we can handle sibling foo.svelte / foo.svelte.ts files and importing both. When doing import Foo from './foo.svelte this now resolves to the Svelte file (prio to #2463 it would resovle to the .svelte.ts file), and doing import Foo from './foo.svelte.js will resolve to the TS file.
1 parent 8c080cf commit a3a7a91

File tree

31 files changed

+215
-50
lines changed

31 files changed

+215
-50
lines changed

packages/language-server/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
dist/
22
.vscode/
33
node_modules/
4+
!test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/node_modules/

packages/language-server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"./bin/server.js": "./bin/server.js"
1111
},
1212
"scripts": {
13-
"test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.ts\" --exclude \"test/**/*.d.ts\"",
13+
"test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.test.ts\"",
1414
"build": "tsc",
1515
"prepublishOnly": "npm run build",
1616
"watch": "tsc -w"

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class ImpliedNodeFormatResolver {
104104
return undefined;
105105
}
106106

107-
let mode = undefined;
107+
let mode: ReturnType<typeof ts.getModeForResolutionAtIndex> = undefined;
108108
if (sourceFile) {
109109
this.cacheImpliedNodeFormat(sourceFile, compilerOptions);
110110
mode = ts.getModeForResolutionAtIndex(sourceFile, importIdxInFile, compilerOptions);

packages/language-server/src/plugins/typescript/svelte-sys.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ export function createSvelteSys(tsSystem: ts.System) {
1111
function svelteFileExists(path: string) {
1212
if (isVirtualSvelteFilePath(path)) {
1313
const sveltePath = toRealSvelteFilePath(path);
14+
15+
// First check if there's a `.svelte.d.ts` or `.d.svelte.ts` file, which should take precedence
16+
const dtsPath = sveltePath.slice(0, -7) + '.svelte.d.ts';
17+
const dtsPathExists = fileExistsCache.get(dtsPath) ?? tsSystem.fileExists(dtsPath);
18+
fileExistsCache.set(dtsPath, dtsPathExists);
19+
if (dtsPathExists) return false;
20+
21+
const svelteDtsPathExists = fileExistsCache.get(path) ?? tsSystem.fileExists(path);
22+
fileExistsCache.set(path, svelteDtsPathExists);
23+
if (svelteDtsPathExists) return false;
24+
1425
const sveltePathExists =
1526
fileExistsCache.get(sveltePath) ?? tsSystem.fileExists(sveltePath);
1627
fileExistsCache.set(sveltePath, sveltePathExists);
@@ -33,10 +44,11 @@ export function createSvelteSys(tsSystem: ts.System) {
3344
svelteFileExists,
3445
getRealSveltePathIfExists,
3546
fileExists(path: string) {
36-
// We need to check both .svelte and .svelte.ts/js because that's how Svelte 5 will likely mark files with runes in them
47+
// We need to check if this is a virtual svelte file
3748
const sveltePathExists = svelteFileExists(path);
38-
const exists =
39-
sveltePathExists || (fileExistsCache.get(path) ?? tsSystem.fileExists(path));
49+
if (sveltePathExists) return true;
50+
51+
const exists = fileExistsCache.get(path) ?? tsSystem.fileExists(path);
4052
fileExistsCache.set(path, exists);
4153
return exists;
4254
},
@@ -66,7 +78,7 @@ export function createSvelteSys(tsSystem: ts.System) {
6678
const realpath = tsSystem.realpath;
6779
svelteSys.realpath = function (path) {
6880
if (svelteFileExists(path)) {
69-
return realpath(toRealSvelteFilePath(path)) + '.ts';
81+
return realpath(toRealSvelteFilePath(path));
7082
}
7183
return realpath(path);
7284
};

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,17 @@ export function isSvelteFilePath(filePath: string) {
7070
}
7171

7272
export function isVirtualSvelteFilePath(filePath: string) {
73-
return filePath.endsWith('.svelte.ts');
73+
return filePath.endsWith('.d.svelte.ts');
7474
}
7575

7676
export function toRealSvelteFilePath(filePath: string) {
77-
return filePath.slice(0, -'.ts'.length);
77+
return filePath.slice(0, -11 /* 'd.svelte.ts'.length */) + 'svelte';
7878
}
7979

80-
export function toVirtualSvelteFilePath(filePath: string) {
81-
return filePath.endsWith('.ts') ? filePath : filePath + '.ts';
80+
export function toVirtualSvelteFilePath(svelteFilePath: string) {
81+
return isVirtualSvelteFilePath(svelteFilePath)
82+
? svelteFilePath
83+
: svelteFilePath.slice(0, -6 /* 'svelte'.length */) + 'd.svelte.ts';
8284
}
8385

8486
export function ensureRealSvelteFilePath(filePath: string) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
{
3+
"code": -1,
4+
"message": "Unexpected token",
5+
"range": {
6+
"end": {
7+
"character": 47,
8+
"line": 12
9+
},
10+
"start": {
11+
"character": 47,
12+
"line": 12
13+
}
14+
},
15+
"severity": 1,
16+
"source": "ts"
17+
}
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[
2+
{
3+
"code": 2307,
4+
"message": "Cannot find module 'package' or its corresponding type declarations.",
5+
"range": {
6+
"end": {
7+
"character": 45,
8+
"line": 1
9+
},
10+
"start": {
11+
"character": 36,
12+
"line": 1
13+
}
14+
},
15+
"severity": 1,
16+
"source": "ts",
17+
"tags": []
18+
},
19+
{
20+
"code": 2307,
21+
"message": "Cannot find module 'package/y' or its corresponding type declarations.",
22+
"range": {
23+
"start": {
24+
"character": 38,
25+
"line": 3
26+
},
27+
"end": {
28+
"character": 49,
29+
"line": 3
30+
}
31+
},
32+
"severity": 1,
33+
"source": "ts",
34+
"tags": []
35+
}
36+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import DefaultSvelteWithTS from 'package'; // with https://github.com/sveltejs/language-tools/pull/2478 this would work; needs decision if we want that
3+
import SubWithDTS from 'package/x';
4+
import SubWithoutDTSAndNotTS from 'package/y';
5+
</script>
6+
7+
<DefaultSvelteWithTS />
8+
<SubWithDTS />
9+
<SubWithoutDTSAndNotTS />

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/node_modules/package/foo.svelte

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/node_modules/package/package.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/node_modules/package/x-types.d.ts

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/node_modules/package/x.svelte

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/exports-map-svelte/node_modules/package/y.svelte

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"module": "esnext",
4+
"target": "esnext",
5+
"moduleResolution": "Bundler"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
export let foo = true;
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare const a: boolean;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
export let foo = true;
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const b = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare const c: boolean;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
export let foo = true;
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
export let foo = true;
3+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const d = true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script lang="ts">
2+
import { a } from './a.svelte';
3+
import B from './b.svelte';
4+
import { b } from './b.svelte.js';
5+
import { c } from './c.svelte';
6+
import D from './d.svelte';
7+
import { d } from './d.svelte.js';
8+
a;
9+
b;
10+
c;
11+
d;
12+
</script>
13+
14+
<B />
15+
<D />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"allowJs": true,
5+
"target": "ESNext",
6+
/**
7+
This is actually not needed, but makes the tests faster
8+
because TS does not look up other types.
9+
*/
10+
"types": ["svelte"],
11+
"allowArbitraryExtensions": true // else .d.svelte.ts will be an error
12+
}
13+
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ describe('createSvelteModuleLoader', () => {
5959
it('uses svelte script kind if resolved module is svelte file', async () => {
6060
const resolvedModule: ts.ResolvedModuleFull = {
6161
extension: ts.Extension.Ts,
62-
resolvedFileName: 'filename.svelte.ts'
62+
resolvedFileName: 'filename.d.svelte.ts'
6363
};
6464
const { getSvelteSnapshotStub, moduleResolver, svelteSys } = setup(resolvedModule);
6565

6666
svelteSys.getRealSveltePathIfExists = (filename: string) =>
67-
filename === 'filename.svelte.ts' ? 'filename.svelte' : filename;
67+
filename === 'filename.d.svelte.ts' ? 'filename.svelte' : filename;
6868

6969
const result = moduleResolver.resolveModuleNames(
7070
['./normal.ts'],

packages/language-server/test/plugins/typescript/svelte-sys.test.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,24 @@ describe('Svelte Sys', () => {
2929
}
3030

3131
describe('#fileExists', () => {
32-
it('should leave files with no .svelte.ts-ending as is', async () => {
32+
it('should leave files with no .d.svelte.ts-ending as is', async () => {
3333
const { loader, fileExistsStub } = setupLoader();
3434
loader.fileExists('../file.ts');
3535

3636
assert.strictEqual(fileExistsStub.getCall(0).args[0], '../file.ts');
3737
});
3838

39-
it('should convert .svelte.ts-endings', async () => {
39+
it('should convert .d.svelte.ts-endings', async () => {
4040
const { loader, fileExistsStub } = setupLoader();
41-
loader.fileExists('../file.svelte.ts');
41+
fileExistsStub.onCall(0).returns(false);
42+
fileExistsStub.onCall(1).returns(false);
43+
fileExistsStub.onCall(2).returns(true);
4244

43-
assert.strictEqual(fileExistsStub.getCall(0).args[0], '../file.svelte');
45+
loader.fileExists('../file.d.svelte.ts');
46+
47+
assert.strictEqual(fileExistsStub.getCall(0).args[0], '../file.svelte.d.ts');
48+
assert.strictEqual(fileExistsStub.getCall(1).args[0], '../file.d.svelte.ts');
49+
assert.strictEqual(fileExistsStub.getCall(2).args[0], '../file.svelte');
4450
});
4551
});
4652
});

packages/typescript-plugin/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "typescript-svelte-plugin",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "A TypeScript Plugin providing Svelte intellisense",
55
"main": "dist/src/index.js",
66
"scripts": {

0 commit comments

Comments
 (0)