fix: adjust Svelte typings to include Svelte 5 function components

Svelte 5 components that only use runes and none of the legacy stuff (events, slots) are typed as function components. This adjusts the types to include that shape.

Fixes #29308
This commit is contained in:
Simon Holthausen 2025-03-14 20:54:50 +01:00
parent 26e8829d09
commit 055410310a
4 changed files with 110 additions and 24 deletions

View File

@ -0,0 +1,15 @@
<script lang="ts">
interface Props {
disabled: boolean;
label: string;
clicked?: (e: MouseEvent) => void
}
// When a Svelte 5 component only uses runes, it is typed as a function component.
// Others are typed as class components. Hence we need to have tests for both shapes.
let { disabled, label, clicked}: Props = $props();
</script>
<button onclick={clicked} {disabled}>
{label}
</button>

View File

@ -5,19 +5,20 @@ import { satisfies } from 'storybook/internal/common';
import type { Canvas, ComponentAnnotations, StoryAnnotations } from 'storybook/internal/types';
import { expectTypeOf } from 'expect-type';
import type { ComponentProps, SvelteComponent } from 'svelte';
import type { Component, ComponentProps, SvelteComponent } from 'svelte';
import Button from './__test__/Button.svelte';
import ButtonV5 from './__test__/ButtonV5.svelte';
import Decorator2 from './__test__/Decorator2.svelte';
import Decorator1 from './__test__/Decorator.svelte';
import type { Decorator, Meta, StoryObj } from './public-types';
import type { SvelteRenderer } from './types';
type SvelteStory<Component extends SvelteComponent, Args, RequiredArgs> = StoryAnnotations<
SvelteRenderer<Component>,
type SvelteStory<
Comp extends SvelteComponent | Component<any, any, any>,
Args,
RequiredArgs
>;
RequiredArgs,
> = StoryAnnotations<SvelteRenderer<Comp>, Args, RequiredArgs>;
describe('Meta', () => {
it('Generic parameter of Meta can be a component', () => {
@ -34,6 +35,20 @@ describe('Meta', () => {
>();
});
it('Generic parameter of Meta can be a Svelte 5 component', () => {
const meta: Meta<typeof ButtonV5> = {
component: ButtonV5,
args: {
label: 'good',
disabled: false,
},
};
expectTypeOf(meta).toMatchTypeOf<
ComponentAnnotations<SvelteRenderer<typeof ButtonV5>, { disabled: boolean; label: string }>
>();
});
it('Generic parameter of Meta can be the props of the component', () => {
const meta: Meta<{ disabled: boolean; label: string }> = {
component: Button,
@ -99,6 +114,21 @@ describe('StoryObj', () => {
expectTypeOf<Actual>().toMatchTypeOf<Expected>();
});
it('✅ Required args may be provided partial in meta and the story (Svelte 5)', () => {
const meta = satisfies<Meta<typeof ButtonV5>>()({
component: ButtonV5,
args: { label: 'good' },
});
type Actual = StoryObj<typeof meta>;
type Expected = SvelteStory<
typeof ButtonV5,
{ disabled: boolean; label: string },
{ disabled: boolean; label?: string }
>;
expectTypeOf<Actual>().toMatchTypeOf<Expected>();
});
it('❌ The combined shape of meta args and story args must match the required args.', () => {
{
const meta = satisfies<Meta<Button>>()({ component: Button });
@ -150,6 +180,16 @@ describe('StoryObj', () => {
>
>();
});
it('Svelte 5 Component can be used as generic parameter for StoryObj', () => {
expectTypeOf<StoryObj<typeof ButtonV5>>().toMatchTypeOf<
SvelteStory<
typeof ButtonV5,
{ disabled: boolean; label: string },
{ disabled?: boolean; label?: string }
>
>();
});
});
type ThemeData = 'light' | 'dark';
@ -243,3 +283,13 @@ it('mount accepts a Component and props', () => {
};
expectTypeOf(Basic).toMatchTypeOf<StoryObj<Button>>();
});
it('mount accepts a Svelte 5 Component and props', () => {
const Basic: StoryObj<typeof ButtonV5> = {
async play({ mount }) {
const canvas = await mount(ButtonV5, { props: { label: 'label', disabled: true } });
expectTypeOf(canvas).toMatchTypeOf<Canvas>();
},
};
expectTypeOf(Basic).toMatchTypeOf<StoryObj<Button>>();
});

View File

@ -15,7 +15,7 @@ import type {
import type { ComponentProps, ComponentType, SvelteComponent } from 'svelte';
import type { SetOptional, Simplify } from 'type-fest';
import type { SvelteRenderer } from './types';
import type { Svelte5ComponentType, SvelteRenderer } from './types';
export type { Args, ArgTypes, Parameters, StrictArgs } from 'storybook/internal/types';
@ -24,19 +24,22 @@ export type { Args, ArgTypes, Parameters, StrictArgs } from 'storybook/internal/
*
* @see [Default export](https://storybook.js.org/docs/api/csf#default-export)
*/
export type Meta<CmpOrArgs = Args> =
CmpOrArgs extends SvelteComponent<infer Props>
? ComponentAnnotations<SvelteRenderer<CmpOrArgs>, Props>
: ComponentAnnotations<SvelteRenderer, CmpOrArgs>;
export type Meta<CmpOrArgs = Args> = CmpOrArgs extends
| SvelteComponent<infer Props>
| Svelte5ComponentType<infer Props>
? ComponentAnnotations<SvelteRenderer<CmpOrArgs>, Props>
: ComponentAnnotations<SvelteRenderer, CmpOrArgs>;
/**
* Story function that represents a CSFv2 component example.
*
* @see [Named Story exports](https://storybook.js.org/docs/api/csf#named-story-exports)
*/
export type StoryFn<TCmpOrArgs = Args> =
TCmpOrArgs extends SvelteComponent<infer Props>
? AnnotatedStoryFn<SvelteRenderer, Props>
: AnnotatedStoryFn<SvelteRenderer, TCmpOrArgs>;
export type StoryFn<TCmpOrArgs = Args> = TCmpOrArgs extends
| SvelteComponent<infer Props>
| Svelte5ComponentType<infer Props>
? AnnotatedStoryFn<SvelteRenderer, Props>
: AnnotatedStoryFn<SvelteRenderer, TCmpOrArgs>;
/**
* Story object that represents a CSFv3 component example.
@ -45,19 +48,32 @@ export type StoryFn<TCmpOrArgs = Args> =
*/
export type StoryObj<MetaOrCmpOrArgs = Args> = MetaOrCmpOrArgs extends {
render?: ArgsStoryFn<SvelteRenderer, any>;
component?: ComponentType<infer Component>;
component?: infer Comp;
args?: infer DefaultArgs;
}
? Simplify<
ComponentProps<Component> & ArgsFromMeta<SvelteRenderer, MetaOrCmpOrArgs>
ComponentProps<
Comp extends ComponentType<infer Component>
? Component
: Comp extends Svelte5ComponentType
? Comp
: never
> &
ArgsFromMeta<SvelteRenderer, MetaOrCmpOrArgs>
> extends infer TArgs
? StoryAnnotations<
SvelteRenderer<Component>,
SvelteRenderer<
Comp extends ComponentType<infer Component>
? Component
: Comp extends Svelte5ComponentType
? Comp
: never
>,
TArgs,
SetOptional<TArgs, Extract<keyof TArgs, keyof DefaultArgs>>
>
: never
: MetaOrCmpOrArgs extends SvelteComponent
: MetaOrCmpOrArgs extends SvelteComponent | Svelte5ComponentType
? StoryAnnotations<SvelteRenderer<MetaOrCmpOrArgs>, ComponentProps<MetaOrCmpOrArgs>>
: StoryAnnotations<SvelteRenderer, MetaOrCmpOrArgs>;

View File

@ -37,14 +37,19 @@ type ComponentType<
>[P];
};
export interface SvelteRenderer<C extends SvelteComponent = SvelteComponent> extends WebRenderer {
component: ComponentType<this['T'] extends Record<string, any> ? this['T'] : any>;
export type Svelte5ComponentType<Props extends Record<string, any> = any> = (typeof import('svelte') extends { mount: any }
? // @ts-ignore svelte.Component doesn't exist in Svelte 4
import('svelte').Component<Props, any, any>
: never)
export interface SvelteRenderer<C extends SvelteComponent | Svelte5ComponentType = SvelteComponent> extends WebRenderer {
component: ComponentType<this['T'] extends Record<string, any> ? this['T'] : any> | Svelte5ComponentType<this['T'] extends Record<string, any> ? this['T'] : any>;
storyResult: this['T'] extends Record<string, any>
? SvelteStoryResult<this['T'], ComponentEvents<C>>
? SvelteStoryResult<this['T'], C extends SvelteComponent ? ComponentEvents<C> : {}>
: SvelteStoryResult;
mount: (
Component?: ComponentType,
Component?: ComponentType | Svelte5ComponentType,
// TODO add proper typesafety
options?: Record<string, any> & { props: Record<string, any> }
) => Promise<Canvas>;
@ -54,10 +59,10 @@ export interface SvelteStoryResult<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
> {
Component?: ComponentType<Props>;
Component?: ComponentType<Props> | Svelte5ComponentType<Props>;
on?: Record<string, any> extends Events
? Record<string, (event: CustomEvent) => void>
: { [K in keyof Events as string extends K ? never : K]?: (event: Events[K]) => void };
props?: Props;
decorator?: ComponentType<Props>;
decorator?: ComponentType<Props> | Svelte5ComponentType<Props>;
}