Skip to content

Commit cad755f

Browse files
authored
(feat) rewrite svelte2tsx (#1237)
Goal: Get rid of a tsx-style transformation and simplify transformations along the way #1077 #1256 #1149 Advantages: - no messing with projects who use JSX for some other reason and then have conflicting JSX definitions - TS control flow easier to keep flowing - overall easier transformations of Svelte specific syntax like await, each etc. - better type inference in some cases, for example around svelte:component This includes: - rewriting the html2jsx part of svelte2tsx - adjusting the language server to the new transformation - adding a toggle to language-server and vs code extension to turn on the new transformation. Default "off" for now, with the plan to switch it to "on" and then removing the old transformation altogether - ensuring tests run with both new and old transformation - adjusting the TypeScript plugin. It uses the new transformation already now since it's not easily possible to add an option (on/off). Should be low-impact since TS doesn't need to know much about the contents of a Svelte file, only its public API Look at the PR of this commit to see more explanations with some of the entry points of the new transformation.
1 parent 42f273e commit cad755f

File tree

416 files changed

+11684
-3432
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

416 files changed

+11684
-3432
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ packages/svelte2tsx/test/**/*
44
packages/svelte2tsx/index.*
55
declare module '*.svelte'
66
packages/svelte2tsx/svelte-shims.d.ts
7+
packages/svelte2tsx/svelte-jsx.d.ts

.prettierignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
packages/svelte2tsx/*.d.ts
2-
packages/svelte2tsx/test/*/samples/*/*sx
3-
packages/svelte2tsx/test/*/samples/*/*.svelte
2+
packages/svelte2tsx/repl/*
3+
packages/svelte2tsx/test/*/samples/**/*
44
packages/svelte2tsx/test/sourcemaps/samples/*
55
packages/svelte2tsx/test/emitDts/samples/*/expected/**
66
packages/language-server/test/**/*.svelte

packages/language-server/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "dist/src/index.js",
66
"typings": "dist/src/index",
77
"scripts": {
8-
"test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.ts\"",
8+
"test": "cross-env TS_NODE_TRANSPILE_ONLY=true mocha --require ts-node/register \"test/**/*.ts\" --exclude \"test/**/*.d.ts\"",
99
"build": "tsc",
1010
"prepublishOnly": "npm run build",
1111
"watch": "tsc -w"
@@ -57,7 +57,7 @@
5757
"source-map": "^0.7.3",
5858
"svelte": "^3.46.1",
5959
"svelte-preprocess": "~4.10.1",
60-
"svelte2tsx": "~0.4.0",
60+
"svelte2tsx": "~0.5.0",
6161
"typescript": "*",
6262
"vscode-css-languageservice": "~5.1.0",
6363
"vscode-emmet-helper": "~2.6.0",

packages/language-server/src/lib/documents/utils.ts

+13
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,19 @@ export function getNodeIfIsInStartTag(html: HTMLDocument, offset: number): Node
344344
}
345345
}
346346

347+
/**
348+
* Returns `true` if `offset` is a html tag and within the name of the start tag or end tag
349+
*/
350+
export function isInHTMLTagRange(html: HTMLDocument, offset: number): boolean {
351+
const node = html.findNodeAt(offset);
352+
return (
353+
!!node.tag &&
354+
node.tag[0] === node.tag[0].toLowerCase() &&
355+
(node.start + node.tag.length + 1 >= offset ||
356+
(!!node.endTagStart && node.endTagStart <= offset))
357+
);
358+
}
359+
347360
/**
348361
* Gets word range at position.
349362
* Delimiter is by default a whitespace, but can be adjusted.

packages/language-server/src/ls-config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const defaultLSConfig: LSConfig = {
4545
},
4646
svelte: {
4747
enable: true,
48+
useNewTransformation: false,
4849
compilerWarnings: {},
4950
diagnostics: { enable: true },
5051
rename: { enable: true },
@@ -176,6 +177,7 @@ export type CompilerWarningsSettings = Record<string, 'ignore' | 'error'>;
176177

177178
export interface LSSvelteConfig {
178179
enable: boolean;
180+
useNewTransformation: boolean;
179181
compilerWarnings: CompilerWarningsSettings;
180182
diagnostics: {
181183
enable: boolean;

packages/language-server/src/plugins/PluginHost.ts

+48-12
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import {
2828
TextEdit,
2929
WorkspaceEdit
3030
} from 'vscode-languageserver';
31-
import { DocumentManager } from '../lib/documents';
31+
import { DocumentManager, getNodeIfIsInHTMLStartTag } from '../lib/documents';
3232
import { Logger } from '../logger';
33-
import { regexLastIndexOf } from '../utils';
33+
import { isNotNullOrUndefined, regexLastIndexOf } from '../utils';
3434
import {
3535
AppCompletionItem,
3636
FileRename,
@@ -115,18 +115,54 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
115115
): Promise<CompletionList> {
116116
const document = this.getDocument(textDocument.uri);
117117

118-
const completions = (
119-
await this.execute<CompletionList>(
120-
'getCompletions',
121-
[document, position, completionContext, cancellationToken],
122-
ExecuteMode.Collect,
123-
'high'
124-
)
125-
).filter((completion) => completion != null);
118+
const completions = await Promise.all(
119+
this.plugins.map(async (plugin) => {
120+
const result = await this.tryExecutePlugin(
121+
plugin,
122+
'getCompletions',
123+
[document, position, completionContext, cancellationToken],
124+
null
125+
);
126+
if (result) {
127+
return { result: result as CompletionList, plugin: plugin.__name };
128+
}
129+
})
130+
).then((completions) => completions.filter(isNotNullOrUndefined));
131+
132+
const html = completions.find((completion) => completion.plugin === 'html');
133+
const ts = completions.find((completion) => completion.plugin === 'ts');
134+
if (html && ts && getNodeIfIsInHTMLStartTag(document.html, document.offsetAt(position))) {
135+
// Completion in a component or html start tag and both html and ts
136+
// suggest something -> filter out all duplicates from TS completions
137+
const htmlCompletions = new Set(html.result.items.map((item) => item.label));
138+
ts.result.items = ts.result.items.filter((item) => {
139+
const label = item.label;
140+
if (htmlCompletions.has(label)) {
141+
return false;
142+
}
143+
if (label[0] === '"' && label[label.length - 1] === '"') {
144+
if (htmlCompletions.has(label.slice(1, -1))) {
145+
// "aria-label" -> aria-label -> exists in html completions
146+
return false;
147+
}
148+
}
149+
if (label.startsWith('on')) {
150+
if (htmlCompletions.has('on:' + label.slice(2))) {
151+
// onclick -> on:click -> exists in html completions
152+
return false;
153+
}
154+
}
155+
// adjust sort text so it does appear after html completions
156+
item.sortText = 'Z' + (item.sortText || '');
157+
return true;
158+
});
159+
}
126160

127-
let flattenedCompletions = flatten(completions.map((completion) => completion.items));
161+
let flattenedCompletions = flatten(
162+
completions.map((completion) => completion.result.items)
163+
);
128164
const isIncomplete = completions.reduce(
129-
(incomplete, completion) => incomplete || completion.isIncomplete,
165+
(incomplete, completion) => incomplete || completion.result.isIncomplete,
130166
false as boolean
131167
);
132168

packages/language-server/src/plugins/css/CSSPlugin.ts

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class CSSPlugin
5454
DocumentSymbolsProvider,
5555
SelectionRangeProvider
5656
{
57+
__name = 'css';
5758
private configManager: LSConfigManager;
5859
private cssDocuments = new WeakMap<Document, CSSDocument>();
5960
private triggerCharacters = ['.', ':', '-', '/'];

packages/language-server/src/plugins/html/HTMLPlugin.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { possiblyComponent } from '../../utils';
3737
export class HTMLPlugin
3838
implements HoverProvider, CompletionsProvider, RenameProvider, LinkedEditingRangesProvider
3939
{
40+
__name = 'html';
4041
private configManager: LSConfigManager;
4142
private lang = getLanguageService({
4243
customDataProviders: [svelteHtmlDataProvider],

packages/language-server/src/plugins/interfaces.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,4 @@ export type Plugin = Partial<
226226
OnWatchFileChanges &
227227
SelectionRangeProvider &
228228
UpdateTsOrJsFile
229-
>;
229+
> & { __name: string };

packages/language-server/src/plugins/svelte/SveltePlugin.ts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class SveltePlugin
4141
CodeActionsProvider,
4242
SelectionRangeProvider
4343
{
44+
__name = 'svelte';
4445
private docManager = new Map<Document, SvelteDocument>();
4546

4647
constructor(private configManager: LSConfigManager) {}

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

+13-3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export interface SnapshotFragment extends DocumentMapper {
7878
*/
7979
export interface SvelteSnapshotOptions {
8080
transformOnTemplateError: boolean;
81+
useNewTransformation: boolean;
82+
typingsNamespace: string;
8183
}
8284

8385
export namespace DocumentSnapshot {
@@ -159,13 +161,21 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
159161
getScriptKindFromAttributes(document.scriptInfo?.attributes ?? {}),
160162
getScriptKindFromAttributes(document.moduleScriptInfo?.attributes ?? {})
161163
].includes(ts.ScriptKind.TSX)
162-
? ts.ScriptKind.TSX
164+
? options.useNewTransformation
165+
? ts.ScriptKind.TS
166+
: ts.ScriptKind.TSX
167+
: options.useNewTransformation
168+
? ts.ScriptKind.JS
163169
: ts.ScriptKind.JSX;
164170

165171
try {
166172
const tsx = svelte2tsx(text, {
167173
filename: document.getFilePath() ?? undefined,
168-
isTsFile: scriptKind === ts.ScriptKind.TSX,
174+
isTsFile: options.useNewTransformation
175+
? scriptKind === ts.ScriptKind.TS
176+
: scriptKind === ts.ScriptKind.TSX,
177+
mode: options.useNewTransformation ? 'ts' : 'tsx',
178+
typingsNamespace: options.useNewTransformation ? options.typingsNamespace : undefined,
169179
emitOnTemplateError: options.transformOnTemplateError,
170180
namespace: document.config?.compilerOptions?.namespace,
171181
accessors:
@@ -388,7 +398,7 @@ export class SvelteSnapshotFragment implements SnapshotFragment {
388398
constructor(
389399
private readonly mapper: DocumentMapper,
390400
public readonly text: string,
391-
private readonly parent: Document,
401+
public readonly parent: Document,
392402
private readonly url: string
393403
) {}
394404

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

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class LSAndTSDocResolver {
7070
return {
7171
ambientTypesSource: this.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
7272
createDocument: this.createDocument,
73+
useNewTransformation: this.configManager.getConfig().svelte.useNewTransformation,
7374
transformOnTemplateError: !this.isSvelteCheck,
7475
globalSnapshotsManager: this.globalSnapshotsManager,
7576
notifyExceedSizeLimit: this.notifyExceedSizeLimit

0 commit comments

Comments
 (0)