Skip to content

Commit

Permalink
fix(svelte5): update typings to support new component types (#400)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous authored Oct 1, 2024
1 parent 2cf781c commit 6f45a96
Show file tree
Hide file tree
Showing 11 changed files with 154 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
# We only need to lint once, so do it on latest Node and Svelte
- { node: '20', svelte: '4', check: 'lint' }
# `SvelteComponent` is not generic in Svelte 3, so type-checking only passes in >= 4
- { node: '20', svelte: '4', check: 'types' }
- { node: '20', svelte: '4', check: 'types:legacy' }
- { node: '20', svelte: 'next', check: 'types' }
# Only run Svelte 5 checks on latest Node
- { node: '20', svelte: 'next', check: 'test:vitest:jsdom' }
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"test:vitest:happy-dom": "vitest run --coverage --environment happy-dom",
"test:jest": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --coverage",
"types": "svelte-check",
"types:legacy": "svelte-check --tsconfig tsconfig.legacy.json",
"validate": "npm-run-all test:vitest:* test:jest types build",
"build": "tsc -p tsconfig.build.json",
"contributors:add": "all-contributors add",
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/fixtures/Mounter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
})
</script>
<button></button>
<button>click me</button>
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
export let name: string
export let count: number
export const hello: string = 'hello'
</script>

<h1>hello {name}</h1>
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/fixtures/TypedRunes.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
const { name, count }: { name: string; count: number } = $props()
export const hello: string = 'hello'
</script>

<h1>hello {name}</h1>
<p>count: {count}</p>
39 changes: 39 additions & 0 deletions src/__tests__/render-runes.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expectTypeOf } from 'expect-type'
import { describe, test } from 'vitest'

import * as subject from '../index.js'
import Component from './fixtures/TypedRunes.svelte'

describe('types', () => {
test('render is a function that accepts a Svelte component', () => {
subject.render(Component, { name: 'Alice', count: 42 })
subject.render(Component, { props: { name: 'Alice', count: 42 } })
})

test('rerender is a function that accepts partial props', async () => {
const { rerender } = subject.render(Component, { name: 'Alice', count: 42 })

await rerender({ name: 'Bob' })
await rerender({ count: 0 })
})

test('invalid prop types are rejected', () => {
// @ts-expect-error: name should be a string
subject.render(Component, { name: 42 })

// @ts-expect-error: name should be a string
subject.render(Component, { props: { name: 42 } })
})

test('render result has container and component', () => {
const result = subject.render(Component, { name: 'Alice', count: 42 })

expectTypeOf(result).toMatchTypeOf<{
container: HTMLElement
component: { hello: string }
debug: (el?: HTMLElement) => void
rerender: (props: { name?: string; count?: number }) => Promise<void>
unmount: () => void
}>()
})
})
Original file line number Diff line number Diff line change
@@ -1,45 +1,12 @@
import { expectTypeOf } from 'expect-type'
import type { ComponentProps, SvelteComponent } from 'svelte'
import { describe, test } from 'vitest'

import * as subject from '../index.js'
import Simple from './fixtures/Simple.svelte'

describe('types', () => {
test('render is a function that accepts a Svelte component', () => {
subject.render(Simple, { name: 'Alice', count: 42 })
subject.render(Simple, { props: { name: 'Alice', count: 42 } })
})

test('rerender is a function that accepts partial props', async () => {
const { rerender } = subject.render(Simple, { name: 'Alice', count: 42 })

await rerender({ name: 'Bob' })
await rerender({ count: 0 })
})

test('invalid prop types are rejected', () => {
// @ts-expect-error: name should be a string
subject.render(Simple, { name: 42 })

// @ts-expect-error: name should be a string
subject.render(Simple, { props: { name: 42 } })
})

test('render result has container and component', () => {
const result = subject.render(Simple, { name: 'Alice', count: 42 })

expectTypeOf(result).toMatchTypeOf<{
container: HTMLElement
component: SvelteComponent<{ name: string }>
debug: (el?: HTMLElement) => void
rerender: (props: Partial<ComponentProps<Simple>>) => Promise<void>
unmount: () => void
}>()
})
import Component from './fixtures/Comp.svelte'

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

expectTypeOf(result.getByRole).parameters.toMatchTypeOf<
[role: subject.ByRoleMatcher, options?: subject.ByRoleOptions]
Expand All @@ -55,8 +22,8 @@ describe('types', () => {
() => ''
)
const result = subject.render(
Simple,
{ name: 'Alice', count: 42 },
Component,
{ name: 'Alice' },
{ queries: { getByVibes } }
)

Expand Down
39 changes: 39 additions & 0 deletions src/__tests__/render.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expectTypeOf } from 'expect-type'
import { describe, test } from 'vitest'

import * as subject from '../index.js'
import Component from './fixtures/Typed.svelte'

describe('types', () => {
test('render is a function that accepts a Svelte component', () => {
subject.render(Component, { name: 'Alice', count: 42 })
subject.render(Component, { props: { name: 'Alice', count: 42 } })
})

test('rerender is a function that accepts partial props', async () => {
const { rerender } = subject.render(Component, { name: 'Alice', count: 42 })

await rerender({ name: 'Bob' })
await rerender({ count: 0 })
})

test('invalid prop types are rejected', () => {
// @ts-expect-error: name should be a string
subject.render(Component, { name: 42 })

// @ts-expect-error: name should be a string
subject.render(Component, { props: { name: 42 } })
})

test('render result has container and component', () => {
const result = subject.render(Component, { name: 'Alice', count: 42 })

expectTypeOf(result).toMatchTypeOf<{
container: HTMLElement
component: { hello: string }
debug: (el?: HTMLElement) => void
rerender: (props: { name?: string; count?: number }) => Promise<void>
unmount: () => void
}>()
})
})
43 changes: 43 additions & 0 deletions src/component-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type * as Svelte from 'svelte'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IS_MODERN_SVELTE = any extends Svelte.Component ? false : true

/** A compiled, imported Svelte component. */
export type Component<P> = IS_MODERN_SVELTE extends true
? Svelte.Component<P> | Svelte.SvelteComponent<P>
: Svelte.SvelteComponent<P>

/**
* The type of an imported, compiled Svelte component.
*
* In Svelte 4, this was the Svelte component class' type.
* In Svelte 5, this distinction no longer matters.
*/
export type ComponentType<C> = C extends Svelte.SvelteComponent
? Svelte.ComponentType<C>
: C

/** The props of a component. */
export type Props<C> = Svelte.ComponentProps<C>

/**
* The exported fields of a component.
*
* In Svelte 4, this is simply the instance of the component class.
* In Svelte 5, this is the set of variables marked as `export`'d.
*/
export type Exports<C> = C extends Svelte.SvelteComponent
? C
: C extends Svelte.Component<unknown, infer E>
? E
: never

/**
* Options that may be passed to `mount` when rendering the component.
*
* In Svelte 4, these are the options passed to the component constructor.
*/
export type MountOptions<C> = IS_MODERN_SVELTE extends true
? Parameters<typeof Svelte.mount<Props<C>, Exports<C>>>[1]
: Svelte.ComponentConstructorOptions<Props<C>>
14 changes: 7 additions & 7 deletions src/pure.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const componentCache = new Set()
/**
* Customize how Svelte renders the component.
*
* @template {import('svelte').SvelteComponent} C
* @typedef {import('svelte').ComponentProps<C> | Partial<import('svelte').ComponentConstructorOptions<import('svelte').ComponentProps<C>>>} SvelteComponentOptions
* @template {import('./component-types.js').Component} C
* @typedef {import('./component-types.js').Props<C> | Partial<import('./component-types.js').MountOptions<C>>} SvelteComponentOptions
*/

/**
Expand All @@ -30,15 +30,15 @@ const componentCache = new Set()
/**
* The rendered component and bound testing functions.
*
* @template {import('svelte').SvelteComponent} C
* @template {import('./component-types.js').Component} C
* @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
*
* @typedef {{
* container: HTMLElement
* baseElement: HTMLElement
* component: C
* component: import('./component-types.js').Exports<C>
* debug: (el?: HTMLElement | DocumentFragment) => void
* rerender: (props: Partial<import('svelte').ComponentProps<C>>) => Promise<void>
* rerender: (props: Partial<import('./component-types.js').Props<C>>) => Promise<void>
* unmount: () => void
* } & {
* [P in keyof Q]: import('@testing-library/dom').BoundFunction<Q[P]>
Expand All @@ -48,10 +48,10 @@ const componentCache = new Set()
/**
* Render a component into the document.
*
* @template {import('svelte').SvelteComponent} C
* @template {import('./component-types.js').Component} C
* @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
*
* @param {import('svelte').ComponentType<C>} Component - The component to render.
* @param {import('./component-types.js').ComponentType<C>} Component - The component to render.
* @param {SvelteComponentOptions<C>} options - Customize how Svelte renders the component.
* @param {RenderOptions<Q>} renderOptions - Customize how Testing Library sets up the document and binds queries.
* @returns {RenderResult<C, Q>} The rendered component and bound testing functions.
Expand Down
8 changes: 8 additions & 0 deletions tsconfig.legacy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": ["./tsconfig.json"],
"exclude": [
"src/__tests__/render-runes.test-d.ts",
"src/__tests__/fixtures/CompRunes.svelte",
"src/__tests__/fixtures/TypedRunes.svelte"
]
}

0 comments on commit 6f45a96

Please sign in to comment.