Skip to content

Commit 505d7dc

Browse files
authored
(feat) TypeScript plugin (#978)
Initially support - rename (doesn't work for prop renames yet) - diagnostics - find references - go to definition - go to implementation(s) This makes all files TSX hardcoded for now, it seems the TS language server is okay with importing tsx into js #580 #550 #342 #110
1 parent c676a77 commit 505d7dc

20 files changed

+1220
-8
lines changed

packages/typescript-plugin/.gitignore

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
src/**/*.js
2-
src/**/*.d.ts
3-
src/tsconfig.tsbuildinfo
1+
dist
42
node_modules
53
tsconfig.tsbuildinfo

packages/typescript-plugin/.npmignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/node_modules
2+
/src
3+
tsconfig.json
4+
.gitignore
5+
internal.md

packages/typescript-plugin/README.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# A TypeScript plugin for Svelte intellisense
2+
3+
This plugin provides intellisense for interacting with Svelte files. It is in a very early stage, so expect bugs. So far the plugin supports
4+
5+
- Rename
6+
- Find Usages
7+
- Go To Definition
8+
- Diagnostics
9+
10+
Note that these features are only available within TS/JS files. Intellisense within Svelte files is provided by the [svelte-language-server](https://www.npmjs.com/package/svelte-language-server).
11+
12+
## Usage
13+
14+
The plugin comes packaged with the [Svelte for VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using that one, you don't need to add it manually.
15+
16+
Adding it manually:
17+
18+
`npm install --save-dev typescript-svelte-plugin`
19+
20+
Then add it to your `tsconfig.json` or `jsconfig.json`:
21+
22+
```
23+
{
24+
"compilerOptions": {
25+
...
26+
"plugins": [{
27+
"name": "typescript-svelte-plugin"
28+
}]
29+
}
30+
}
31+
```
32+
33+
## Limitations
34+
35+
Changes to Svelte files are only recognized after they are saved to disk.
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Notes on how this works internally
2+
3+
To get a general understanding on how to write a TypeScript plugin, read [this how-to](https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin).
4+
5+
However, for getting Svelte support inside TS/JS files, we need to do more than what's shown in the how-to. We need to
6+
7+
- make TypeScript aware that Svelte files exist and can be loaded
8+
- present Svelte files to TypeScript in a way TypeScript understands
9+
- enhance the language service (the part that's shown in the how-to)
10+
11+
To make TypeScript aware of Svelte files, we need to patch its module resolution algorithm. `.svelte` is not a valid file ending for TypeScript, so it searches for files like `.svelte.ts`. This logic is decorated in `src/module-loader` to also resolve Svelte files. They are resolved to file-type TSX/JSX, which leads us to the next obstacle: to present Svelte files to TypeScript in a way it understands.
12+
13+
We achieve that by utilizing `svelte2tsx`, which transforms Svelte code into TSX/JSX code. We do that transformation by patching `readFile` of TypeScript's project service in `src/svelte-snapshots`: If a Svelte file is read, transform the code before returning it. During that we also patch the ScriptInfo that TypeScript uses to interact with files. We patch the methods that transform positions to offsets and vice versa and either do transforms on the generated or original code, depending on the situation.
14+
15+
The last step is to enhance the language service. For that, we patch the desired methods and apply custom logic. Most of that is transforming the generated code positions to the original code positions.
16+
17+
Along the way, we need to patch some internal methods, which is brittly and hacky, but to our knowledge there currently is no other way.
18+
19+
## Limitations
20+
21+
Currently, changes to Svelte files are only recognized after they are saved to disk. That could be changed by adding `"languages": ["svelte"]` to the plugin provide options. The huge disadvantage is that diagnostics, rename etc within Svelte files no longer stay in the control of the language-server, instead TS/JS starts interacting with Svelte files on a much deeper level, which would mean patching many more undocumented/private methods, and having less control of the situation overall.

packages/typescript-plugin/package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
"name": "typescript-svelte-plugin",
33
"version": "0.1.0",
44
"description": "A TypeScript Plugin providing Svelte intellisense",
5-
"main": "src/index.js",
5+
"main": "dist/src/index.js",
66
"scripts": {
77
"build": "tsc -p ./",
8+
"watch": "tsc -w -p ./",
89
"test": "echo 'NOOP'"
910
},
1011
"keywords": [
@@ -19,5 +20,9 @@
1920
"@tsconfig/node12": "^1.0.0",
2021
"@types/node": "^13.9.0",
2122
"typescript": "*"
23+
},
24+
"dependencies": {
25+
"svelte2tsx": "*",
26+
"sourcemap-codec": "^1.4.4"
2227
}
2328
}

packages/typescript-plugin/src/index.ts

+49-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
1-
function init(modules: { typescript: typeof import('typescript/lib/tsserverlibrary') }) {
1+
import { dirname, resolve } from 'path';
2+
import { decorateLanguageService } from './language-service';
3+
import { Logger } from './logger';
4+
import { patchModuleLoader } from './module-loader';
5+
import { SvelteSnapshotManager } from './svelte-snapshots';
6+
import type ts from 'typescript/lib/tsserverlibrary';
7+
8+
function init(modules: { typescript: typeof ts }) {
29
function create(info: ts.server.PluginCreateInfo) {
3-
// TODO
10+
const logger = new Logger(info.project.projectService.logger);
11+
logger.log('Starting Svelte plugin');
12+
13+
const snapshotManager = new SvelteSnapshotManager(
14+
modules.typescript,
15+
info.project.projectService,
16+
logger,
17+
!!info.project.getCompilerOptions().strict
18+
);
19+
20+
patchCompilerOptions(info.project);
21+
patchModuleLoader(
22+
logger,
23+
snapshotManager,
24+
modules.typescript,
25+
info.languageServiceHost,
26+
info.project
27+
);
28+
return decorateLanguageService(info.languageService, snapshotManager, logger);
429
}
530

631
function getExternalFiles(project: ts.server.ConfiguredProject) {
7-
// TODO
32+
// Needed so the ambient definitions are known inside the tsx files
33+
const svelteTsPath = dirname(require.resolve('svelte2tsx'));
34+
const svelteTsxFiles = [
35+
'./svelte-shims.d.ts',
36+
'./svelte-jsx.d.ts',
37+
'./svelte-native-jsx.d.ts'
38+
].map((f) => modules.typescript.sys.resolvePath(resolve(svelteTsPath, f)));
39+
return svelteTsxFiles;
40+
}
41+
42+
function patchCompilerOptions(project: ts.server.Project) {
43+
const compilerOptions = project.getCompilerOptions();
44+
// Patch needed because svelte2tsx creates jsx/tsx files
45+
compilerOptions.jsx = modules.typescript.JsxEmit.Preserve;
46+
47+
// detect which JSX namespace to use (svelte | svelteNative) if not specified or not compatible
48+
if (!compilerOptions.jsxFactory || !compilerOptions.jsxFactory.startsWith('svelte')) {
49+
// Default to regular svelte, this causes the usage of the "svelte.JSX" namespace
50+
// We don't need to add a switch for svelte-native because the jsx is only relevant
51+
// within Svelte files, which this plugin does not deal with.
52+
compilerOptions.jsxFactory = 'svelte.createElement';
53+
}
854
}
955

1056
return { create, getExternalFiles };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { Logger } from '../logger';
3+
import { isSvelteFilePath, replaceDeep } from '../utils';
4+
5+
const componentPostfix = '__SvelteComponent_';
6+
7+
export function decorateCompletions(ls: ts.LanguageService, logger: Logger): void {
8+
const getCompletionsAtPosition = ls.getCompletionsAtPosition;
9+
ls.getCompletionsAtPosition = (fileName, position, options) => {
10+
const completions = getCompletionsAtPosition(fileName, position, options);
11+
if (!completions) {
12+
return completions;
13+
}
14+
return {
15+
...completions,
16+
entries: completions.entries.map((entry) => {
17+
if (
18+
!isSvelteFilePath(entry.source || '') ||
19+
!entry.name.endsWith(componentPostfix)
20+
) {
21+
return entry;
22+
}
23+
return {
24+
...entry,
25+
name: entry.name.slice(0, -componentPostfix.length)
26+
};
27+
})
28+
};
29+
};
30+
31+
const getCompletionEntryDetails = ls.getCompletionEntryDetails;
32+
ls.getCompletionEntryDetails = (
33+
fileName,
34+
position,
35+
entryName,
36+
formatOptions,
37+
source,
38+
preferences
39+
) => {
40+
const details = getCompletionEntryDetails(
41+
fileName,
42+
position,
43+
entryName,
44+
formatOptions,
45+
source,
46+
preferences
47+
);
48+
if (details || !isSvelteFilePath(source || '')) {
49+
return details;
50+
}
51+
52+
// In the completion list we removed the component postfix. Internally,
53+
// the language service saved the list with the postfix, so details
54+
// won't match anything. Therefore add it back and remove it afterwards again.
55+
const svelteDetails = getCompletionEntryDetails(
56+
fileName,
57+
position,
58+
entryName + componentPostfix,
59+
formatOptions,
60+
source,
61+
preferences
62+
);
63+
if (!svelteDetails) {
64+
return undefined;
65+
}
66+
logger.debug('Found Svelte Component import completion details');
67+
68+
return replaceDeep(svelteDetails, componentPostfix, '');
69+
};
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { Logger } from '../logger';
3+
import { SvelteSnapshotManager } from '../svelte-snapshots';
4+
import { isNotNullOrUndefined, isSvelteFilePath } from '../utils';
5+
6+
export function decorateGetDefinition(
7+
ls: ts.LanguageService,
8+
snapshotManager: SvelteSnapshotManager,
9+
logger: Logger
10+
): void {
11+
const getDefinitionAndBoundSpan = ls.getDefinitionAndBoundSpan;
12+
ls.getDefinitionAndBoundSpan = (fileName, position) => {
13+
const definition = getDefinitionAndBoundSpan(fileName, position);
14+
if (!definition?.definitions) {
15+
return definition;
16+
}
17+
18+
return {
19+
...definition,
20+
definitions: definition.definitions
21+
.map((def) => {
22+
if (!isSvelteFilePath(def.fileName)) {
23+
return def;
24+
}
25+
26+
const textSpan = snapshotManager
27+
.get(def.fileName)
28+
?.getOriginalTextSpan(def.textSpan);
29+
if (!textSpan) {
30+
return undefined;
31+
}
32+
return {
33+
...def,
34+
textSpan,
35+
// Spare the work for now
36+
originalTextSpan: undefined,
37+
contextSpan: undefined,
38+
originalContextSpan: undefined
39+
};
40+
})
41+
.filter(isNotNullOrUndefined)
42+
};
43+
};
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type ts from 'typescript/lib/tsserverlibrary';
2+
import { Logger } from '../logger';
3+
import { isSvelteFilePath } from '../utils';
4+
5+
export function decorateDiagnostics(ls: ts.LanguageService, logger: Logger): void {
6+
decorateSyntacticDiagnostics(ls);
7+
decorateSemanticDiagnostics(ls);
8+
decorateSuggestionDiagnostics(ls);
9+
}
10+
11+
function decorateSyntacticDiagnostics(ls: ts.LanguageService): void {
12+
const getSyntacticDiagnostics = ls.getSyntacticDiagnostics;
13+
ls.getSyntacticDiagnostics = (fileName: string) => {
14+
// Diagnostics inside Svelte files are done
15+
// by the svelte-language-server / Svelte for VS Code extension
16+
if (isSvelteFilePath(fileName)) {
17+
return [];
18+
}
19+
return getSyntacticDiagnostics(fileName);
20+
};
21+
}
22+
23+
function decorateSemanticDiagnostics(ls: ts.LanguageService): void {
24+
const getSemanticDiagnostics = ls.getSemanticDiagnostics;
25+
ls.getSemanticDiagnostics = (fileName: string) => {
26+
// Diagnostics inside Svelte files are done
27+
// by the svelte-language-server / Svelte for VS Code extension
28+
if (isSvelteFilePath(fileName)) {
29+
return [];
30+
}
31+
return getSemanticDiagnostics(fileName);
32+
};
33+
}
34+
35+
function decorateSuggestionDiagnostics(ls: ts.LanguageService): void {
36+
const getSuggestionDiagnostics = ls.getSuggestionDiagnostics;
37+
ls.getSuggestionDiagnostics = (fileName: string) => {
38+
// Diagnostics inside Svelte files are done
39+
// by the svelte-language-server / Svelte for VS Code extension
40+
if (isSvelteFilePath(fileName)) {
41+
return [];
42+
}
43+
return getSuggestionDiagnostics(fileName);
44+
};
45+
}

0 commit comments

Comments
 (0)