Skip to content

Commit b0d3f69

Browse files
authored
(feat) add experimental $$Events and strictEvents support (#1054)
$$Events lets the user define all events that a component can dispatch. If createEventDispatcher is not typed, the $$Events definition is added to it under the hood for type checking. strictEvents can be used when the user is sure that there are no other events besides the one from createEventDispatcher and forwarded events - this removes the custom event fallback typing. #442 #424 sveltejs/rfcs#38
1 parent 1992d63 commit b0d3f69

File tree

31 files changed

+480
-81
lines changed

31 files changed

+480
-81
lines changed

packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,4 +1293,117 @@ describe('DiagnosticsProvider', () => {
12931293
}
12941294
]);
12951295
});
1296+
1297+
it('checks $$Events usage', async () => {
1298+
const { plugin, document } = setup('$$events.svelte');
1299+
const diagnostics = await plugin.getDiagnostics(document);
1300+
assert.deepStrictEqual(diagnostics, [
1301+
{
1302+
code: 2345,
1303+
message:
1304+
"Argument of type 'true' is not assignable to parameter of type 'string | undefined'.",
1305+
range: {
1306+
start: {
1307+
character: 20,
1308+
line: 12
1309+
},
1310+
end: {
1311+
character: 24,
1312+
line: 12
1313+
}
1314+
},
1315+
severity: 1,
1316+
source: 'ts',
1317+
tags: []
1318+
},
1319+
{
1320+
code: 2345,
1321+
message:
1322+
'Argument of type \'"click"\' is not assignable to parameter of type \'"foo"\'.',
1323+
range: {
1324+
start: {
1325+
character: 13,
1326+
line: 13
1327+
},
1328+
end: {
1329+
character: 20,
1330+
line: 13
1331+
}
1332+
},
1333+
severity: 1,
1334+
source: 'ts',
1335+
tags: []
1336+
}
1337+
]);
1338+
});
1339+
1340+
it('checks $$Events component usage', async () => {
1341+
const { plugin, document } = setup('diagnostics-$$events.svelte');
1342+
const diagnostics = await plugin.getDiagnostics(document);
1343+
assert.deepStrictEqual(diagnostics, [
1344+
{
1345+
code: 2345,
1346+
message:
1347+
// Note: If you only run this test, the test message is slightly different for some reason
1348+
'Argument of type \'"bar"\' is not assignable to parameter of type \'"foo" | "click"\'.',
1349+
range: {
1350+
start: {
1351+
character: 10,
1352+
line: 7
1353+
},
1354+
end: {
1355+
character: 15,
1356+
line: 7
1357+
}
1358+
},
1359+
severity: 1,
1360+
source: 'ts',
1361+
tags: []
1362+
},
1363+
{
1364+
code: 2367,
1365+
message:
1366+
"This condition will always return 'false' since the types 'string' and 'boolean' have no overlap.",
1367+
range: {
1368+
start: {
1369+
character: 37,
1370+
line: 7
1371+
},
1372+
end: {
1373+
character: 54,
1374+
line: 7
1375+
}
1376+
},
1377+
severity: 1,
1378+
source: 'ts',
1379+
tags: []
1380+
}
1381+
]);
1382+
});
1383+
1384+
it('checks strictEvents', async () => {
1385+
const { plugin, document } = setup('diagnostics-strictEvents.svelte');
1386+
const diagnostics = await plugin.getDiagnostics(document);
1387+
assert.deepStrictEqual(diagnostics, [
1388+
{
1389+
code: 2345,
1390+
message:
1391+
// Note: If you only run this test, the test message is slightly different for some reason
1392+
'Argument of type \'"bar"\' is not assignable to parameter of type \'"foo" | "click"\'.',
1393+
range: {
1394+
start: {
1395+
character: 16,
1396+
line: 7
1397+
},
1398+
end: {
1399+
character: 21,
1400+
line: 7
1401+
}
1402+
},
1403+
severity: 1,
1404+
source: 'ts',
1405+
tags: []
1406+
}
1407+
]);
1408+
});
12961409
});

packages/language-server/test/plugins/typescript/testfiles/completions/component-events-interface.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
interface ComponentEvents {
2+
interface $$Events {
33
a: CustomEvent<boolean>;
44
/**
55
* TEST
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script lang="ts">
2+
import { createEventDispatcher } from 'svelte';
3+
4+
interface $$Events {
5+
foo: CustomEvent<string>;
6+
click: MouseEvent;
7+
}
8+
9+
const dispatch = createEventDispatcher();
10+
// valid
11+
dispatch('foo', 'bar');
12+
// invalid
13+
dispatch('foo', true);
14+
dispatch('click');
15+
</script>
16+
17+
<button on:click>click</button>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
import Events from './$$events.svelte';
3+
</script>
4+
5+
<!-- valid -->
6+
<Events on:click={e => e} on:foo={e => e.detail === 'bar'} />
7+
<!-- invalid -->
8+
<Events on:bar={e => e} on:foo={e => e.detail === true} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
import StrictEvents from './strictEvents.svelte';
3+
</script>
4+
5+
<!-- valid -->
6+
<StrictEvents on:foo={e => e} on:click={e => e} />
7+
<!-- invalid -->
8+
<StrictEvents on:bar={e => e} />
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script strictEvents>
2+
import { createEventDispatcher } from 'svelte';
3+
4+
const dispatch = createEventDispatcher();
5+
dispatch('foo', 'bar');
6+
</script>
7+
8+
<button on:click>click</button>

packages/language-server/test/plugins/typescript/testfiles/hover/hover-events-interface.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
interface ComponentEvents {
2+
interface $$Events {
33
a: CustomEvent<boolean>;
44
/**
55
* TEST

packages/svelte2tsx/src/htmlxtojsx/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export function htmlx2jsx(
229229
htmlx: string,
230230
options?: { emitOnTemplateError?: boolean; preserveAttributeCase: boolean }
231231
) {
232-
const ast = parseHtmlx(htmlx, options);
232+
const ast = parseHtmlx(htmlx, options).htmlxAst;
233233
const str = new MagicString(htmlx);
234234

235235
convertHtmlxToJsx(str, ast, null, null, options);

packages/svelte2tsx/src/svelte2tsx/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function processSvelteTemplate(
5353
str: MagicString,
5454
options?: { emitOnTemplateError?: boolean; namespace?: string }
5555
): TemplateProcessResult {
56-
const htmlxAst = parseHtmlx(str.original, options);
56+
const { htmlxAst, tags } = parseHtmlx(str.original, options);
5757

5858
let uses$$props = false;
5959
let uses$$restProps = false;
@@ -279,7 +279,11 @@ function processSvelteTemplate(
279279
moduleScriptTag,
280280
scriptTag,
281281
slots: slotHandler.getSlotDef(),
282-
events: new ComponentEvents(eventHandler),
282+
events: new ComponentEvents(
283+
eventHandler,
284+
tags.some((tag) => tag.attributes?.some((a) => a.name === 'strictEvents')),
285+
str
286+
),
283287
uses$$props,
284288
uses$$restProps,
285289
uses$$slots,
@@ -461,7 +465,7 @@ export function svelte2tsx(
461465
addComponentExport({
462466
str,
463467
uses$$propsOr$$restProps: uses$$props || uses$$restProps,
464-
strictEvents: events.hasInterface(),
468+
strictEvents: events.hasStrictEvents(),
465469
isTsFile: options?.isTsFile,
466470
getters,
467471
exportedNames,

0 commit comments

Comments
 (0)