Skip to content

Commit 6f45a96

Browse files
authored
fix(svelte5): update typings to support new component types (#400)
1 parent 2cf781c commit 6f45a96

11 files changed

+154
-47
lines changed

Diff for: .github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
# We only need to lint once, so do it on latest Node and Svelte
3535
- { node: '20', svelte: '4', check: 'lint' }
3636
# `SvelteComponent` is not generic in Svelte 3, so type-checking only passes in >= 4
37-
- { node: '20', svelte: '4', check: 'types' }
37+
- { node: '20', svelte: '4', check: 'types:legacy' }
3838
- { node: '20', svelte: 'next', check: 'types' }
3939
# Only run Svelte 5 checks on latest Node
4040
- { node: '20', svelte: 'next', check: 'test:vitest:jsdom' }

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"test:vitest:happy-dom": "vitest run --coverage --environment happy-dom",
7171
"test:jest": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --coverage",
7272
"types": "svelte-check",
73+
"types:legacy": "svelte-check --tsconfig tsconfig.legacy.json",
7374
"validate": "npm-run-all test:vitest:* test:jest types build",
7475
"build": "tsc -p tsconfig.build.json",
7576
"contributors:add": "all-contributors add",

Diff for: src/__tests__/fixtures/Mounter.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
})
1717
</script>
1818
19-
<button></button>
19+
<button>click me</button>

Diff for: src/__tests__/fixtures/Simple.svelte renamed to src/__tests__/fixtures/Typed.svelte

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script lang="ts">
22
export let name: string
33
export let count: number
4+
5+
export const hello: string = 'hello'
46
</script>
57

68
<h1>hello {name}</h1>

Diff for: src/__tests__/fixtures/TypedRunes.svelte

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
const { name, count }: { name: string; count: number } = $props()
3+
4+
export const hello: string = 'hello'
5+
</script>
6+
7+
<h1>hello {name}</h1>
8+
<p>count: {count}</p>

Diff for: src/__tests__/render-runes.test-d.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expectTypeOf } from 'expect-type'
2+
import { describe, test } from 'vitest'
3+
4+
import * as subject from '../index.js'
5+
import Component from './fixtures/TypedRunes.svelte'
6+
7+
describe('types', () => {
8+
test('render is a function that accepts a Svelte component', () => {
9+
subject.render(Component, { name: 'Alice', count: 42 })
10+
subject.render(Component, { props: { name: 'Alice', count: 42 } })
11+
})
12+
13+
test('rerender is a function that accepts partial props', async () => {
14+
const { rerender } = subject.render(Component, { name: 'Alice', count: 42 })
15+
16+
await rerender({ name: 'Bob' })
17+
await rerender({ count: 0 })
18+
})
19+
20+
test('invalid prop types are rejected', () => {
21+
// @ts-expect-error: name should be a string
22+
subject.render(Component, { name: 42 })
23+
24+
// @ts-expect-error: name should be a string
25+
subject.render(Component, { props: { name: 42 } })
26+
})
27+
28+
test('render result has container and component', () => {
29+
const result = subject.render(Component, { name: 'Alice', count: 42 })
30+
31+
expectTypeOf(result).toMatchTypeOf<{
32+
container: HTMLElement
33+
component: { hello: string }
34+
debug: (el?: HTMLElement) => void
35+
rerender: (props: { name?: string; count?: number }) => Promise<void>
36+
unmount: () => void
37+
}>()
38+
})
39+
})

Diff for: src/__tests__/types.test-d.ts renamed to src/__tests__/render-utilities.test-d.ts

+5-38
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,12 @@
11
import { expectTypeOf } from 'expect-type'
2-
import type { ComponentProps, SvelteComponent } from 'svelte'
32
import { describe, test } from 'vitest'
43

54
import * as subject from '../index.js'
6-
import Simple from './fixtures/Simple.svelte'
7-
8-
describe('types', () => {
9-
test('render is a function that accepts a Svelte component', () => {
10-
subject.render(Simple, { name: 'Alice', count: 42 })
11-
subject.render(Simple, { props: { name: 'Alice', count: 42 } })
12-
})
13-
14-
test('rerender is a function that accepts partial props', async () => {
15-
const { rerender } = subject.render(Simple, { name: 'Alice', count: 42 })
16-
17-
await rerender({ name: 'Bob' })
18-
await rerender({ count: 0 })
19-
})
20-
21-
test('invalid prop types are rejected', () => {
22-
// @ts-expect-error: name should be a string
23-
subject.render(Simple, { name: 42 })
24-
25-
// @ts-expect-error: name should be a string
26-
subject.render(Simple, { props: { name: 42 } })
27-
})
28-
29-
test('render result has container and component', () => {
30-
const result = subject.render(Simple, { name: 'Alice', count: 42 })
31-
32-
expectTypeOf(result).toMatchTypeOf<{
33-
container: HTMLElement
34-
component: SvelteComponent<{ name: string }>
35-
debug: (el?: HTMLElement) => void
36-
rerender: (props: Partial<ComponentProps<Simple>>) => Promise<void>
37-
unmount: () => void
38-
}>()
39-
})
5+
import Component from './fixtures/Comp.svelte'
406

7+
describe('render query and utility types', () => {
418
test('render result has default queries', () => {
42-
const result = subject.render(Simple, { name: 'Alice', count: 42 })
9+
const result = subject.render(Component, { name: 'Alice' })
4310

4411
expectTypeOf(result.getByRole).parameters.toMatchTypeOf<
4512
[role: subject.ByRoleMatcher, options?: subject.ByRoleOptions]
@@ -55,8 +22,8 @@ describe('types', () => {
5522
() => ''
5623
)
5724
const result = subject.render(
58-
Simple,
59-
{ name: 'Alice', count: 42 },
25+
Component,
26+
{ name: 'Alice' },
6027
{ queries: { getByVibes } }
6128
)
6229

Diff for: src/__tests__/render.test-d.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expectTypeOf } from 'expect-type'
2+
import { describe, test } from 'vitest'
3+
4+
import * as subject from '../index.js'
5+
import Component from './fixtures/Typed.svelte'
6+
7+
describe('types', () => {
8+
test('render is a function that accepts a Svelte component', () => {
9+
subject.render(Component, { name: 'Alice', count: 42 })
10+
subject.render(Component, { props: { name: 'Alice', count: 42 } })
11+
})
12+
13+
test('rerender is a function that accepts partial props', async () => {
14+
const { rerender } = subject.render(Component, { name: 'Alice', count: 42 })
15+
16+
await rerender({ name: 'Bob' })
17+
await rerender({ count: 0 })
18+
})
19+
20+
test('invalid prop types are rejected', () => {
21+
// @ts-expect-error: name should be a string
22+
subject.render(Component, { name: 42 })
23+
24+
// @ts-expect-error: name should be a string
25+
subject.render(Component, { props: { name: 42 } })
26+
})
27+
28+
test('render result has container and component', () => {
29+
const result = subject.render(Component, { name: 'Alice', count: 42 })
30+
31+
expectTypeOf(result).toMatchTypeOf<{
32+
container: HTMLElement
33+
component: { hello: string }
34+
debug: (el?: HTMLElement) => void
35+
rerender: (props: { name?: string; count?: number }) => Promise<void>
36+
unmount: () => void
37+
}>()
38+
})
39+
})

Diff for: src/component-types.d.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type * as Svelte from 'svelte'
2+
3+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4+
type IS_MODERN_SVELTE = any extends Svelte.Component ? false : true
5+
6+
/** A compiled, imported Svelte component. */
7+
export type Component<P> = IS_MODERN_SVELTE extends true
8+
? Svelte.Component<P> | Svelte.SvelteComponent<P>
9+
: Svelte.SvelteComponent<P>
10+
11+
/**
12+
* The type of an imported, compiled Svelte component.
13+
*
14+
* In Svelte 4, this was the Svelte component class' type.
15+
* In Svelte 5, this distinction no longer matters.
16+
*/
17+
export type ComponentType<C> = C extends Svelte.SvelteComponent
18+
? Svelte.ComponentType<C>
19+
: C
20+
21+
/** The props of a component. */
22+
export type Props<C> = Svelte.ComponentProps<C>
23+
24+
/**
25+
* The exported fields of a component.
26+
*
27+
* In Svelte 4, this is simply the instance of the component class.
28+
* In Svelte 5, this is the set of variables marked as `export`'d.
29+
*/
30+
export type Exports<C> = C extends Svelte.SvelteComponent
31+
? C
32+
: C extends Svelte.Component<unknown, infer E>
33+
? E
34+
: never
35+
36+
/**
37+
* Options that may be passed to `mount` when rendering the component.
38+
*
39+
* In Svelte 4, these are the options passed to the component constructor.
40+
*/
41+
export type MountOptions<C> = IS_MODERN_SVELTE extends true
42+
? Parameters<typeof Svelte.mount<Props<C>, Exports<C>>>[1]
43+
: Svelte.ComponentConstructorOptions<Props<C>>

Diff for: src/pure.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const componentCache = new Set()
1313
/**
1414
* Customize how Svelte renders the component.
1515
*
16-
* @template {import('svelte').SvelteComponent} C
17-
* @typedef {import('svelte').ComponentProps<C> | Partial<import('svelte').ComponentConstructorOptions<import('svelte').ComponentProps<C>>>} SvelteComponentOptions
16+
* @template {import('./component-types.js').Component} C
17+
* @typedef {import('./component-types.js').Props<C> | Partial<import('./component-types.js').MountOptions<C>>} SvelteComponentOptions
1818
*/
1919

2020
/**
@@ -30,15 +30,15 @@ const componentCache = new Set()
3030
/**
3131
* The rendered component and bound testing functions.
3232
*
33-
* @template {import('svelte').SvelteComponent} C
33+
* @template {import('./component-types.js').Component} C
3434
* @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
3535
*
3636
* @typedef {{
3737
* container: HTMLElement
3838
* baseElement: HTMLElement
39-
* component: C
39+
* component: import('./component-types.js').Exports<C>
4040
* debug: (el?: HTMLElement | DocumentFragment) => void
41-
* rerender: (props: Partial<import('svelte').ComponentProps<C>>) => Promise<void>
41+
* rerender: (props: Partial<import('./component-types.js').Props<C>>) => Promise<void>
4242
* unmount: () => void
4343
* } & {
4444
* [P in keyof Q]: import('@testing-library/dom').BoundFunction<Q[P]>
@@ -48,10 +48,10 @@ const componentCache = new Set()
4848
/**
4949
* Render a component into the document.
5050
*
51-
* @template {import('svelte').SvelteComponent} C
51+
* @template {import('./component-types.js').Component} C
5252
* @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
5353
*
54-
* @param {import('svelte').ComponentType<C>} Component - The component to render.
54+
* @param {import('./component-types.js').ComponentType<C>} Component - The component to render.
5555
* @param {SvelteComponentOptions<C>} options - Customize how Svelte renders the component.
5656
* @param {RenderOptions<Q>} renderOptions - Customize how Testing Library sets up the document and binds queries.
5757
* @returns {RenderResult<C, Q>} The rendered component and bound testing functions.

Diff for: tsconfig.legacy.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": ["./tsconfig.json"],
3+
"exclude": [
4+
"src/__tests__/render-runes.test-d.ts",
5+
"src/__tests__/fixtures/CompRunes.svelte",
6+
"src/__tests__/fixtures/TypedRunes.svelte"
7+
]
8+
}

0 commit comments

Comments
 (0)