From e4189b36697d0b9a151d11727e33f352c77bab3a Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Tue, 1 Oct 2024 14:24:51 -0400 Subject: [PATCH] fix(svelte5): update typings to support new component types --- .github/workflows/release.yml | 2 +- package.json | 1 + src/__tests__/fixtures/Mounter.svelte | 2 +- .../fixtures/{Simple.svelte => Typed.svelte} | 2 + src/__tests__/fixtures/TypedRunes.svelte | 8 ++++ src/__tests__/render-runes.test-d.ts | 39 +++++++++++++++++ ...s.test-d.ts => render-utilities.test-d.ts} | 43 +++---------------- src/__tests__/render.test-d.ts | 39 +++++++++++++++++ src/component-types.d.ts | 43 +++++++++++++++++++ src/pure.js | 14 +++--- tsconfig.legacy.json | 8 ++++ 11 files changed, 154 insertions(+), 47 deletions(-) rename src/__tests__/fixtures/{Simple.svelte => Typed.svelte} (76%) create mode 100644 src/__tests__/fixtures/TypedRunes.svelte create mode 100644 src/__tests__/render-runes.test-d.ts rename src/__tests__/{types.test-d.ts => render-utilities.test-d.ts} (55%) create mode 100644 src/__tests__/render.test-d.ts create mode 100644 src/component-types.d.ts create mode 100644 tsconfig.legacy.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9dbf325..8b2ac4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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' } diff --git a/package.json b/package.json index 3fbcde7..e536ce0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/fixtures/Mounter.svelte b/src/__tests__/fixtures/Mounter.svelte index 27205dd..68f72f9 100644 --- a/src/__tests__/fixtures/Mounter.svelte +++ b/src/__tests__/fixtures/Mounter.svelte @@ -16,4 +16,4 @@ }) - + diff --git a/src/__tests__/fixtures/Simple.svelte b/src/__tests__/fixtures/Typed.svelte similarity index 76% rename from src/__tests__/fixtures/Simple.svelte rename to src/__tests__/fixtures/Typed.svelte index c9c2f15..44960ab 100644 --- a/src/__tests__/fixtures/Simple.svelte +++ b/src/__tests__/fixtures/Typed.svelte @@ -1,6 +1,8 @@

hello {name}

diff --git a/src/__tests__/fixtures/TypedRunes.svelte b/src/__tests__/fixtures/TypedRunes.svelte new file mode 100644 index 0000000..979be41 --- /dev/null +++ b/src/__tests__/fixtures/TypedRunes.svelte @@ -0,0 +1,8 @@ + + +

hello {name}

+

count: {count}

diff --git a/src/__tests__/render-runes.test-d.ts b/src/__tests__/render-runes.test-d.ts new file mode 100644 index 0000000..2d0c69f --- /dev/null +++ b/src/__tests__/render-runes.test-d.ts @@ -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 + unmount: () => void + }>() + }) +}) diff --git a/src/__tests__/types.test-d.ts b/src/__tests__/render-utilities.test-d.ts similarity index 55% rename from src/__tests__/types.test-d.ts rename to src/__tests__/render-utilities.test-d.ts index 077bbcd..b45614e 100644 --- a/src/__tests__/types.test-d.ts +++ b/src/__tests__/render-utilities.test-d.ts @@ -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>) => Promise - 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] @@ -55,8 +22,8 @@ describe('types', () => { () => '' ) const result = subject.render( - Simple, - { name: 'Alice', count: 42 }, + Component, + { name: 'Alice' }, { queries: { getByVibes } } ) diff --git a/src/__tests__/render.test-d.ts b/src/__tests__/render.test-d.ts new file mode 100644 index 0000000..f8d1d90 --- /dev/null +++ b/src/__tests__/render.test-d.ts @@ -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 + unmount: () => void + }>() + }) +}) diff --git a/src/component-types.d.ts b/src/component-types.d.ts new file mode 100644 index 0000000..a349597 --- /dev/null +++ b/src/component-types.d.ts @@ -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

= IS_MODERN_SVELTE extends true + ? Svelte.Component

| Svelte.SvelteComponent

+ : Svelte.SvelteComponent

+ +/** + * 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 extends Svelte.SvelteComponent + ? Svelte.ComponentType + : C + +/** The props of a component. */ +export type Props = Svelte.ComponentProps + +/** + * 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 extends Svelte.SvelteComponent + ? C + : C extends Svelte.Component + ? 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 = IS_MODERN_SVELTE extends true + ? Parameters, Exports>>[1] + : Svelte.ComponentConstructorOptions> diff --git a/src/pure.js b/src/pure.js index edb94b3..875f87c 100644 --- a/src/pure.js +++ b/src/pure.js @@ -13,8 +13,8 @@ const componentCache = new Set() /** * Customize how Svelte renders the component. * - * @template {import('svelte').SvelteComponent} C - * @typedef {import('svelte').ComponentProps | Partial>>} SvelteComponentOptions + * @template {import('./component-types.js').Component} C + * @typedef {import('./component-types.js').Props | Partial>} SvelteComponentOptions */ /** @@ -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 * debug: (el?: HTMLElement | DocumentFragment) => void - * rerender: (props: Partial>) => Promise + * rerender: (props: Partial>) => Promise * unmount: () => void * } & { * [P in keyof Q]: import('@testing-library/dom').BoundFunction @@ -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} Component - The component to render. + * @param {import('./component-types.js').ComponentType} Component - The component to render. * @param {SvelteComponentOptions} options - Customize how Svelte renders the component. * @param {RenderOptions} renderOptions - Customize how Testing Library sets up the document and binds queries. * @returns {RenderResult} The rendered component and bound testing functions. diff --git a/tsconfig.legacy.json b/tsconfig.legacy.json new file mode 100644 index 0000000..b5d9e57 --- /dev/null +++ b/tsconfig.legacy.json @@ -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" + ] +}