Skip to content

Commit 932b8d3

Browse files
authored
(feat) implement generics attr on script tags (#2020)
sveltejs/rfcs#38 (comment)
1 parent 2f25562 commit 932b8d3

File tree

17 files changed

+260
-52
lines changed

17 files changed

+260
-52
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,13 @@ const addAttributes: Record<string, IAttributeData[]> = {
363363
{
364364
name: 'bind:open'
365365
}
366+
],
367+
script: [
368+
{
369+
name: 'generics',
370+
description:
371+
'Generics used within the components. Only available when using TypeScript.'
372+
}
366373
]
367374
};
368375

packages/language-server/src/plugins/typescript/features/SemanticTokensProvider.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
7070
textDocument,
7171
tsDoc,
7272
generatedOffset,
73-
generatedLength
73+
generatedLength,
74+
encodedClassification
7475
);
7576
if (!originalPosition) {
7677
continue;
@@ -106,14 +107,14 @@ export class SemanticTokensProviderImpl implements SemanticTokensProvider {
106107
document: Document,
107108
snapshot: SvelteDocumentSnapshot,
108109
generatedOffset: number,
109-
generatedLength: number
110+
generatedLength: number,
111+
token: number
110112
): [line: number, character: number, length: number, start: number] | undefined {
113+
const text = snapshot.getFullText();
111114
if (
112-
isInGeneratedCode(
113-
snapshot.getFullText(),
114-
generatedOffset,
115-
generatedOffset + generatedLength
116-
)
115+
isInGeneratedCode(text, generatedOffset, generatedOffset + generatedLength) ||
116+
(token === 2817 /* top level function */ &&
117+
text.substring(generatedOffset, generatedOffset + generatedLength) === 'render')
117118
) {
118119
return;
119120
}

packages/svelte-vscode/syntaxes/svelte.tmLanguage.src.yaml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,32 @@ repository:
464464
end: (?<=[^\s=])(?!\s*=)|(?=/?>)
465465
patterns: [include: '#attributes-value']
466466

467+
# Matches the generics attribute on script tags
468+
attributes-generics:
469+
begin: (generics)(=)(["'])
470+
beginCaptures:
471+
1: { name: entity.other.attribute-name.svelte }
472+
2: { name: punctuation.separator.key-value.svelte }
473+
3: { name: punctuation.definition.string.begin.svelte }
474+
end: (\3)
475+
endCaptures:
476+
1: { name: punctuation.definition.string.end.svelte }
477+
contentName: meta.embedded.expression.svelte source.ts
478+
patterns: [ include: '#type-parameters' ]
479+
480+
# Copied over from https://github.com/microsoft/TypeScript-TmLanguage/blob/master/TypeScript.YAML-tmLanguage#L2308
481+
# and removed the start/end matches which have the < and > included, which are not present in our case
482+
type-parameters:
483+
name: meta.type.parameters.ts
484+
patterns:
485+
- include: 'source.ts#comment'
486+
- name: storage.modifier.ts
487+
match: '(?<![_$[:alnum:]])(?:(?<=\.\.\.)|(?<!\.))(extends|in|out|const)(?![_$[:alnum:]])(?:(?=\.\.\.)|(?!\.))'
488+
- include: 'source.ts#type'
489+
- include: 'source.ts#punctuation-comma'
490+
- name: keyword.operator.assignment.ts
491+
match: (=)(?!>)
492+
467493
# ------
468494
# TAGS
469495

@@ -509,7 +535,9 @@ repository:
509535
end: (?=/>)|>
510536
endCaptures: { 0: { name: punctuation.definition.tag.end.svelte } }
511537
name: meta.tag.start.svelte
512-
patterns: [ include: '#attributes' ]
538+
patterns:
539+
- include: '#attributes-generics'
540+
- include: '#attributes'
513541

514542
# Matches the beginning (`<name`) section of a tag start node.
515543
tags-start-node:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts" generics="
2+
T extends 'a' |'b' |'c'
3+
">
4+
export let t: T;
5+
</script>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
><script lang="ts" generics="
2+
#^ source.svelte meta.script.svelte meta.tag.start.svelte punctuation.definition.tag.begin.svelte
3+
# ^^^^^^ source.svelte meta.script.svelte meta.tag.start.svelte entity.name.tag.svelte
4+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
5+
# ^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte entity.other.attribute-name.svelte
6+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte punctuation.separator.key-value.svelte
7+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.begin.svelte
8+
# ^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte
9+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.end.svelte
10+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
11+
# ^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte entity.other.attribute-name.svelte
12+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.separator.key-value.svelte
13+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.begin.svelte
14+
> T extends 'a' |'b' |'c'
15+
#^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
16+
# ^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts storage.modifier.ts
17+
# ^^^^^^^^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
18+
> ">
19+
#^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
20+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.end.svelte
21+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
22+
> export let t: T;
23+
#^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.embedded.block.svelte source.ts
24+
></script>
25+
#^^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
26+
# ^^^^^^ source.svelte meta.script.svelte meta.tag.end.svelte entity.name.tag.svelte
27+
# ^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
28+
>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script generics="T" lang="ts">
2+
export let t: T;
3+
</script>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
><script generics="T" lang="ts">
2+
#^ source.svelte meta.script.svelte meta.tag.start.svelte punctuation.definition.tag.begin.svelte
3+
# ^^^^^^ source.svelte meta.script.svelte meta.tag.start.svelte entity.name.tag.svelte
4+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
5+
# ^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte entity.other.attribute-name.svelte
6+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.separator.key-value.svelte
7+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.begin.svelte
8+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.embedded.expression.svelte source.ts
9+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.string.end.svelte
10+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte
11+
# ^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte entity.other.attribute-name.svelte
12+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte punctuation.separator.key-value.svelte
13+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.begin.svelte
14+
# ^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte
15+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte meta.attribute.lang.svelte string.quoted.svelte punctuation.definition.string.end.svelte
16+
# ^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.tag.start.svelte punctuation.definition.tag.end.svelte
17+
> export let t: T;
18+
#^^^^^^^^^^^^^^^^^^^^^ source.svelte meta.script.svelte meta.lang.ts.svelte meta.embedded.block.svelte source.ts
19+
></script>
20+
#^^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.begin.svelte
21+
# ^^^^^^ source.svelte meta.script.svelte meta.tag.end.svelte entity.name.tag.svelte
22+
# ^ source.svelte meta.script.svelte meta.tag.end.svelte punctuation.definition.tag.end.svelte
23+
>

packages/svelte2tsx/src/svelte2tsx/createRenderFunction.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,28 +63,37 @@ export function createRenderFunction({
6363
//I couldn't get magicstring to let me put the script before the <> we prepend during conversion of the template to jsx, so we just close it instead
6464
const scriptTagEnd = htmlx.lastIndexOf('>', scriptTag.content.start) + 1;
6565
str.overwrite(scriptTag.start, scriptTag.start + 1, ';');
66-
str.overwrite(
67-
scriptTag.start + 1,
68-
scriptTagEnd,
69-
`function render${generics.toDefinitionString(true)}() {${propsDecl}\n`
70-
);
66+
if (generics.genericsAttr) {
67+
let start = generics.genericsAttr.value[0].start;
68+
let end = generics.genericsAttr.value[0].end;
69+
if (htmlx.charAt(start) === '"' || htmlx.charAt(start) === "'") {
70+
start++;
71+
end--;
72+
}
73+
str.overwrite(scriptTag.start + 1, start - 1, `function render`);
74+
str.overwrite(start - 1, start, `<`); // if the generics are unused, only this char is colored opaque
75+
if (end < scriptTagEnd) {
76+
str.overwrite(end, scriptTagEnd, `>() {${propsDecl}\n`);
77+
} else {
78+
str.prependRight(end, `>() {${propsDecl}\n`);
79+
}
80+
} else {
81+
str.overwrite(
82+
scriptTag.start + 1,
83+
scriptTagEnd,
84+
`function render${generics.toDefinitionString(true)}() {${propsDecl}\n`
85+
);
86+
}
7187

7288
const scriptEndTagStart = htmlx.lastIndexOf('<', scriptTag.end - 1);
7389
// wrap template with callback
74-
str.overwrite(
75-
scriptEndTagStart,
76-
scriptTag.end,
77-
`${slotsDeclaration};\nasync () => {`,
78-
79-
{
80-
contentOnly: true
81-
}
82-
);
90+
str.overwrite(scriptEndTagStart, scriptTag.end, `${slotsDeclaration};\nasync () => {`, {
91+
contentOnly: true
92+
});
8393
} else {
8494
str.prependRight(
8595
scriptDestination,
86-
`;function render${generics.toDefinitionString(true)}() {` +
87-
`${propsDecl}${slotsDeclaration}\nasync () => {`
96+
`;function render() {` + `${propsDecl}${slotsDeclaration}\nasync () => {`
8897
);
8998
}
9099

packages/svelte2tsx/src/svelte2tsx/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ export function svelte2tsx(
355355
const implicitStoreValues = new ImplicitStoreValues(resolvedStores, renderFunctionStart);
356356
//move the instance script and process the content
357357
let exportedNames = new ExportedNames(str, 0, basename);
358-
let generics = new Generics(str, 0);
358+
let generics = new Generics(str, 0, { attributes: [] } as any);
359359
let uses$$SlotsInterface = false;
360360
if (scriptTag) {
361361
//ensure it is between the module script and the rest of the template (the variables need to be declared before the jsx template)

packages/svelte2tsx/src/svelte2tsx/nodes/Generics.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
import MagicString from 'magic-string';
22
import ts from 'typescript';
3+
import { Node } from 'estree-walker';
34
import { surroundWithIgnoreComments } from '../../utils/ignore';
45
import { throwError } from '../utils/error';
56

67
export class Generics {
78
private definitions: string[] = [];
89
private typeReferences: string[] = [];
910
private references: string[] = [];
11+
genericsAttr: Node | undefined;
1012

11-
constructor(private str: MagicString, private astOffset: number) {}
13+
constructor(private str: MagicString, private astOffset: number, script: Node) {
14+
this.genericsAttr = script.attributes.find((attr) => attr.name === 'generics');
15+
const generics = this.genericsAttr?.value[0]?.raw as string | undefined;
16+
if (generics) {
17+
this.definitions = generics.split(',').map((g) => g.trim());
18+
this.references = this.definitions.map((def) => def.split(/\s/)[0]);
19+
} else {
20+
this.genericsAttr = undefined;
21+
}
22+
}
1223

1324
addIfIsGeneric(node: ts.Node) {
1425
if (ts.isTypeAliasDeclaration(node) && this.is$$GenericType(node.type)) {
26+
if (this.genericsAttr) {
27+
throw new Error(
28+
'Invalid $$Generic declaration: $$Generic definitions are not allowed when the generics attribute is present on the script tag'
29+
);
30+
}
1531
if (node.type.typeArguments?.length > 1) {
1632
throw new Error('Invalid $$Generic declaration: Only one type argument allowed');
1733
}

0 commit comments

Comments
 (0)