@storybook/addon-svelte-csf 5.1.2 β†’ 5.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,34 @@
1
1
  # Svelte CSF
2
2
 
3
- This Storybook addon allows you to write Storybook stories using the Svelte language instead of ESM that regular CSF is based on.
4
-
5
- ```bash
6
- npx storybook@latest add @storybook/addon-svelte-csf
7
- ```
8
-
9
- Using the Svelte language makes it easier to write stories for composed components that rely on snippets or slots, which aren't easily re-created outside of Svelte files.
3
+ The **Svelte CSF** addon lets you write [Storybook](https://storybook.js.org/) stories directly in `.svelte` files instead of `.js` or `.ts` files.
4
+ By leveraging native Svelte syntax and reactivity, it provides a more natural and seamless way to document and showcase your Svelte components directly within Storybook.
10
5
 
11
6
  ## 🐣 Getting Started
12
7
 
13
8
  > [!TIP]
14
9
  > If you've initialized your Storybook project with Storybook version 8.2.0 or above, this addon is already set up for you!
15
10
 
16
- > [!IMPORTANT]
11
+ > [!IMPORTANT]
17
12
  > Not running the latest and greatest versions of Storybook or Svelte? Be sure to check [the version compatibility section below](#version-compatibility).
18
13
 
19
- The easiest way to install the addon is with `storybook add`:
14
+ ### Sveltekit
15
+
16
+ If you’re using SvelteKit, you can set up Storybook with the [official Svelte CLI](https://svelte.dev/docs/cli/storybook):
17
+
18
+ ```bash
19
+ npx sv add storybook
20
+ ```
21
+
22
+ ### Storybook CLI
23
+
24
+ Install via the storybook CLI:
20
25
 
21
26
  ```bash
22
27
  npx storybook@latest add @storybook/addon-svelte-csf
23
28
  ```
24
29
 
30
+ ### Manual Setup
31
+
25
32
  You can also add the addon manually. First, install the package:
26
33
 
27
34
  ```bash
@@ -39,7 +46,7 @@ export default {
39
46
  ...
40
47
  ],
41
48
  ...
42
- }
49
+ };
43
50
  ```
44
51
 
45
52
  Restart your Storybook server for the changes to take effect.
@@ -47,11 +54,22 @@ Restart your Storybook server for the changes to take effect.
47
54
  ## πŸ“ Usage
48
55
 
49
56
  > [!NOTE]
50
- > The documentation here does not cover all of Storybook's features, only the aspects that are specific to the addon and Svelte CSF. We recommend that you familiarize yourself with Storybook's core concepts at <https://storybook.js.org/docs>.
57
+ > This documentation only covers Storybook features specific to this addon. We recommend that you familiarize yourself with Storybook's core concepts at <https://storybook.js.org/docs>.
51
58
 
52
- The [`examples`](./examples/) directory contains examples describing each feature of the addon. The [`Button.stories.svelte` example](./examples/Button.stories.svelte) is a good one to get started with. [The Storybook with all the examples is published on Chromatic here](https://next--667492d3e52064f1d418ec95.chromatic.com).
59
+ > [!NOTE]
60
+ > Svelte CSF stories files must always have the `.stories.svelte` extension.
61
+
62
+ - Official tutorial using `Svelte`. ( [link](https://storybook.js.org/tutorials/intro-to-storybook/svelte/en/get-started/) )
63
+ - Getting started with `Sveltekit`. ( [link](https://storybook.js.org/docs/get-started/frameworks/sveltekit) )
64
+ - Getting started with `Svelte`. ( [link](https://storybook.js.org/docs/get-started/frameworks/svelte-vite) )
65
+ - A published storybook. ( [chromatic](https://next--667492d3e52064f1d418ec95.chromatic.com) / [src](./examples/) )
53
66
 
54
- Svelte CSF stories files must always have the `.stories.svelte` extension.
67
+ - [Storybook for Svelte Part 1: Intro](https://youtu.be/GQLgq09-knM?t=2635)
68
+ - [Storybook for Svelte Part 2: Docs](https://youtu.be/h0MHSpLRx_o?t=2957)
69
+ - [Storybook for Svelte Part 3: Testing](https://youtu.be/P8jdPBKwq54?t=2167)
70
+
71
+ - [Adding Storybook to sveltesociety.dev](https://www.youtube.com/live/3u-PXSbU5iA)
72
+ - [Storybook is actually good now, with Jeppe Reinhold](https://youtu.be/TDTS74Bz6Eo)
55
73
 
56
74
  ### Defining the meta
57
75
 
@@ -119,7 +137,7 @@ If you need more customization of the story, like composing components or defini
119
137
  </Story>
120
138
  ```
121
139
 
122
- > [!IMPORTANT]
140
+ > [!IMPORTANT]
123
141
  > This format completely ignores args, as they are not passed down to any of the child components defined. Even if your story has args and Controls, they won't have an effect. See the snippet-based formats below.
124
142
 
125
143
  #### Inline snippet
@@ -35,6 +35,18 @@ export async function preTransformPlugin() {
35
35
  ast: svelteAST,
36
36
  filename: id,
37
37
  });
38
+ // NOTE: Printing the AST again collapses the whitespace between nodes,
39
+ // which would also remove the newlines from the stories' source code snippets.
40
+ // So only print it when a codemod changed something.
41
+ if (transformedSvelteAST === svelteAST) {
42
+ return {
43
+ code,
44
+ map: null,
45
+ meta: {
46
+ _storybook_csf_pre_transform: code,
47
+ },
48
+ };
49
+ }
38
50
  let magicCode = new MagicString(code);
39
51
  magicCode.overwrite(0, code.length - 1, print(transformedSvelteAST));
40
52
  const stringifiedMagicCode = magicCode.toString();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import pkg from '@storybook/addon-svelte-csf/package.json' with { type: 'json' };
2
+ import dedent from 'dedent';
3
+ import { describe, it } from 'vitest';
4
+ import { preTransformPlugin } from './plugins.js';
5
+ async function runPreTransform(code) {
6
+ const plugin = await preTransformPlugin();
7
+ const { handler } = plugin.transform;
8
+ return handler.call({}, code, '/Button.stories.svelte');
9
+ }
10
+ describe(preTransformPlugin.name, () => {
11
+ it('keeps the original code when there is no legacy syntax', async ({ expect }) => {
12
+ const code = dedent(`
13
+ <script module>
14
+ import { defineMeta } from "${pkg.name}";
15
+ import Button from "./Button.svelte";
16
+
17
+ const { Story } = defineMeta({ component: Button });
18
+ </script>
19
+
20
+ <Story name="Custom template">
21
+ {#snippet template(args)}
22
+ <Button {...args}>First</Button>
23
+ <Button {...args}>Second</Button>
24
+ {/snippet}
25
+ </Story>
26
+ `);
27
+ const result = await runPreTransform(code);
28
+ expect(result.code).toBe(code);
29
+ expect(result.meta._storybook_csf_pre_transform).toBe(code);
30
+ });
31
+ it('transforms legacy syntax', async ({ expect }) => {
32
+ const code = dedent(`
33
+ <script context="module">
34
+ import { Story } from "${pkg.name}";
35
+ import Button from "./Button.svelte";
36
+
37
+ export const meta = { component: Button };
38
+ </script>
39
+
40
+ <Story name="Default" />
41
+ `);
42
+ const result = await runPreTransform(code);
43
+ expect(result.code).toContain('defineMeta');
44
+ expect(result.meta._storybook_csf_pre_transform).toBe(result.code);
45
+ });
46
+ });
@@ -17,7 +17,15 @@ export interface State {
17
17
  * We store it to ensure there's no more than one - we will throw error if there's more.
18
18
  */
19
19
  unidentifiedTemplateComponent?: SvelteAST.Component;
20
+ /**
21
+ * Set when the stories file uses any legacy syntax that needs a codemod.
22
+ */
23
+ isLegacy?: boolean;
20
24
  }
25
+ /**
26
+ * Transform the legacy syntax in a stories file to the current syntax.
27
+ * If the file has no legacy syntax, it returns the given AST unchanged.
28
+ */
21
29
  export declare function codemodLegacyNodes(params: Params): Promise<SvelteAST.Root>;
22
30
  interface ComponentIdentifierName {
23
31
  Meta?: string;
@@ -6,6 +6,10 @@ import { transformLegacyStory } from './codemods/legacy-story.js';
6
6
  import { transformTemplateToSnippet } from './codemods/template-to-snippet.js';
7
7
  import { createASTScript } from '../../parser/ast.js';
8
8
  import { DuplicatedUnidentifiedTemplateError } from '../../utils/error/legacy-api/index.js';
9
+ /**
10
+ * Transform the legacy syntax in a stories file to the current syntax.
11
+ * If the file has no legacy syntax, it returns the given AST unchanged.
12
+ */
9
13
  export async function codemodLegacyNodes(params) {
10
14
  const { walk } = await import('zimmerframe');
11
15
  const { ast, filename } = params;
@@ -46,6 +50,10 @@ export async function codemodLegacyNodes(params) {
46
50
  const { state } = context;
47
51
  if (node.source.value === pkg.name) {
48
52
  state.componentIdentifierName = getComponentsIdentifiersNames(node.specifiers);
53
+ if (state.currentScript !== 'module' ||
54
+ Object.keys(state.componentIdentifierName).length > 0) {
55
+ state.isLegacy = true;
56
+ }
49
57
  const transformed = transformImportDeclaration({ node, filename });
50
58
  if (state.currentScript !== 'module') {
51
59
  // NOTE: We store current node in AST walker state.
@@ -66,6 +74,7 @@ export async function codemodLegacyNodes(params) {
66
74
  declaration.declarations[0].id.name === 'meta')) {
67
75
  return;
68
76
  }
77
+ state.isLegacy = true;
69
78
  const transformed = transformExportMetaToDefineMeta(node);
70
79
  const { currentScript } = state;
71
80
  if (currentScript === 'instance') {
@@ -121,6 +130,9 @@ export async function codemodLegacyNodes(params) {
121
130
  }
122
131
  },
123
132
  });
133
+ if (!state.isLegacy) {
134
+ return ast;
135
+ }
124
136
  // Clean-up
125
137
  // TODO: To optimize it (stop walking on AST again)...
126
138
  // it might be possible to use `visit(path(-1))` for some cases in the previous AST walk,
@@ -253,4 +253,19 @@ describe(codemodLegacyNodes.name, () => {
253
253
  </script>"
254
254
  `);
255
255
  });
256
+ it('returns the same AST when there is no legacy syntax', async ({ expect }) => {
257
+ const code = dedent(`
258
+ <script module>
259
+ import { defineMeta } from "${pkg.name}";
260
+ import Button from "./Button.svelte";
261
+
262
+ const { Story } = defineMeta({ component: Button });
263
+ </script>
264
+
265
+ <Story name="Default" />
266
+ `);
267
+ const ast = getSvelteAST({ code });
268
+ const transformed = await codemodLegacyNodes({ ast });
269
+ expect(transformed).toBe(ast);
270
+ });
256
271
  });
@@ -10,14 +10,31 @@ import { DefaultOrNamespaceImportUsedError, GetDefineMetaFirstArgumentError, Mis
10
10
  import { NoDestructuredDefineMetaCallError } from '../utils/error/parser/analyse/define-meta.js';
11
11
  import { StoryTemplateAndChildrenError, StoryTemplateAndAsChildError, StoryAsChildWithoutChildrenError, } from '../utils/error/parser/analyse/story.js';
12
12
  import { extractStoryTemplateSnippetBlock } from '../parser/extract/svelte/story/template.js';
13
+ /**
14
+ * The indexer runs once per `*.stories.svelte` file, while the Svelte config is per-project state.
15
+ * Cache the lookup so the config-file scan runs once per process instead of once per story file β€”
16
+ * without this, projects without a `svelte.config.js` get one
17
+ * "no Svelte config found ... using default configuration" log line per story file.
18
+ * Trade-off: adding or changing a svelte.config file while `storybook dev` runs requires a restart to be picked up.
19
+ */
20
+ let svelteConfigPromise;
21
+ async function loadCachedSvelteConfig() {
22
+ svelteConfigPromise ??= import('@sveltejs/vite-plugin-svelte')
23
+ .then(({ loadSvelteConfig }) => loadSvelteConfig())
24
+ .catch((error) => {
25
+ // Don't cache failures (e.g. a broken svelte.config.js), so the next indexing run retries.
26
+ svelteConfigPromise = undefined;
27
+ throw error;
28
+ });
29
+ return svelteConfigPromise;
30
+ }
13
31
  export async function parseForIndexer(filename, options) {
14
- let [code, { walk }, { loadSvelteConfig }] = await Promise.all([
32
+ let [code, { walk }, svelteConfig] = await Promise.all([
15
33
  fs.readFile(filename, { encoding: 'utf8' }),
16
34
  import('zimmerframe'),
17
- import('@sveltejs/vite-plugin-svelte'),
35
+ loadCachedSvelteConfig(),
18
36
  ]);
19
37
  const { legacyTemplate } = options;
20
- const svelteConfig = await loadSvelteConfig();
21
38
  if (svelteConfig?.preprocess) {
22
39
  code = (await preprocess(code, svelteConfig.preprocess, {
23
40
  filename: filename,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { beforeEach, describe, it, vi } from 'vitest';
4
+ const loadSvelteConfig = vi.hoisted(() => vi.fn(async () => undefined));
5
+ vi.mock('@sveltejs/vite-plugin-svelte', () => ({ loadSvelteConfig }));
6
+ const currentDir = dirname(fileURLToPath(import.meta.url));
7
+ const storyFile = resolve(currentDir, '../../examples/Button.stories.svelte');
8
+ describe('parseForIndexer', () => {
9
+ beforeEach(() => {
10
+ // The config cache lives in module scope β€” reset modules so each test starts cold
11
+ vi.resetModules();
12
+ loadSvelteConfig.mockClear();
13
+ });
14
+ it('loads the Svelte config once across multiple story files', async ({ expect }) => {
15
+ const { parseForIndexer } = await import('./parser.js');
16
+ const files = [storyFile, resolve(currentDir, '../../examples/ExportName.stories.svelte')];
17
+ for (const file of files) {
18
+ await parseForIndexer(file, { legacyTemplate: false });
19
+ }
20
+ expect(loadSvelteConfig).toHaveBeenCalledTimes(1);
21
+ });
22
+ it('retries the Svelte config lookup after a failure instead of caching it', async ({ expect, }) => {
23
+ const { parseForIndexer } = await import('./parser.js');
24
+ loadSvelteConfig.mockRejectedValueOnce(new Error('broken config'));
25
+ await expect(parseForIndexer(storyFile, { legacyTemplate: false })).rejects.toThrow('broken config');
26
+ await expect(parseForIndexer(storyFile, { legacyTemplate: false })).resolves.toBeDefined();
27
+ expect(loadSvelteConfig).toHaveBeenCalledTimes(2);
28
+ });
29
+ });
@@ -123,10 +123,14 @@ export function getStoryContentRawCode(params) {
123
123
  nodes: svelte,
124
124
  filename,
125
125
  });
126
- // NOTE: It should never be `undefined` in this particular case, otherwise Storybook wouldn't know what to render.
127
- return dedent(`<${defineMetaComponentValue?.name} {...args}>
126
+ // If there's no component defined in defineMeta, return the raw code directly
127
+ // This handles static templates without a component wrapper
128
+ if (!defineMetaComponentValue) {
129
+ return rawCode;
130
+ }
131
+ return dedent(`<${defineMetaComponentValue.name} {...args}>
128
132
  ${rawCode}
129
- </${defineMetaComponentValue?.name}>`);
133
+ </${defineMetaComponentValue.name}>`);
130
134
  }
131
135
  /**
132
136
  * Extract from the original code a string slice with the body of the svelte's snippet block.
@@ -259,5 +259,34 @@ describe(getStoryContentRawCode.name, () => {
259
259
  });
260
260
  expect(rawSource).toBe(`<SomeComponent wins="inner-template" {...args} />`);
261
261
  });
262
+ it('works when a static children content provided without component in defineMeta', async ({ expect, }) => {
263
+ const code = `
264
+ <script module>
265
+ import { defineMeta } from "@storybook/addon-svelte-csf";
266
+
267
+ const { Story } = defineMeta({
268
+ title: 'Templating',
269
+ });
270
+ </script>
271
+
272
+ <Story name="Static template">
273
+ <h2 data-testid="heading">Static template</h2>
274
+ <p>This story is static</p>
275
+ </Story>
276
+ `;
277
+ const ast = getSvelteAST({ code });
278
+ const svelteASTNodes = await extractSvelteASTNodes({ ast });
279
+ const { storyComponents } = svelteASTNodes;
280
+ const component = storyComponents[0].component;
281
+ const rawSource = getStoryContentRawCode({
282
+ nodes: {
283
+ component,
284
+ svelte: svelteASTNodes,
285
+ },
286
+ originalCode: code,
287
+ });
288
+ expect(rawSource).toBe(dedent `<h2 data-testid="heading">Static template</h2>
289
+ <p>This story is static</p>`);
290
+ });
262
291
  });
263
292
  });
@@ -12,9 +12,13 @@ Vite pre-transform hook does codemod where this component gets transformed into
12
12
  <script lang="ts">
13
13
  import type { Slots, StoryProps } from '../legacy-types.d.ts';
14
14
 
15
- type $$Props = StoryProps;
15
+ // Props use `$props()` so this file compiles when `compilerOptions.runes` is `true`.
16
+ // It still renders a `<slot>`, because the typing of legacy `let:args` and `let:context` comes from `$$Slots`.
16
17
  type $$Slots = Slots;
17
18
 
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ const _props: StoryProps = $props();
21
+
18
22
  let args!: $$Slots['default']['args'];
19
23
  let context!: $$Slots['default']['context'];
20
24
  </script>
@@ -3,7 +3,7 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
3
3
  new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
4
  $$bindings?: Bindings;
5
5
  } & Exports;
6
- (internal: unknown, props: {
6
+ (internal: unknown, props: Props & {
7
7
  $$events?: Events;
8
8
  $$slots?: Slots;
9
9
  }): Exports & {
@@ -28,6 +28,6 @@ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
28
28
  */
29
29
  declare const LegacyStory: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<StoryProps, Slots>, {
30
30
  [evt: string]: CustomEvent<any>;
31
- }, Slots, {}, string>;
31
+ }, Slots, {}, "">;
32
32
  type LegacyStory = InstanceType<typeof LegacyStory>;
33
33
  export default LegacyStory;
@@ -11,13 +11,13 @@ Vite pre-transform hook does codemod where this component gets transformed into
11
11
  <script lang="ts">
12
12
  import type { Slots, TemplateProps } from '../legacy-types.d.ts';
13
13
 
14
- export let id: string = 'default';
15
- // silents the Svelte warning about 'id' being unused. It's only here for typing purposes.
16
- const silentWarningAboutId = id;
17
-
18
- type $$Props = TemplateProps;
14
+ // Props use `$props()` so this file compiles when `compilerOptions.runes` is `true`.
15
+ // It still renders a `<slot>`, because the typing of legacy `let:args` and `let:context` comes from `$$Slots`.
19
16
  type $$Slots = Slots;
20
17
 
18
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
+ const _props: TemplateProps = $props();
20
+
21
21
  let args!: $$Slots['default']['args'];
22
22
  let context!: $$Slots['default']['context'];
23
23
  </script>
@@ -28,6 +28,6 @@ type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
28
28
  */
29
29
  declare const LegacyTemplate: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<TemplateProps, Slots>, {
30
30
  [evt: string]: CustomEvent<any>;
31
- }, Slots, {}, string>;
31
+ }, Slots, {}, "">;
32
32
  type LegacyTemplate = InstanceType<typeof LegacyTemplate>;
33
33
  export default LegacyTemplate;
@@ -46,8 +46,17 @@ const skipSourceRender = (context) => {
46
46
  return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE;
47
47
  };
48
48
  export const generateCodeToEmit = ({ code, args }) => {
49
+ // check if children is used in the template (e.g., {args.children})
50
+ // if so, we should exclude it from the props to avoid it appearing twice
51
+ const childrenUsedInTemplate = /\bargs\??\.children\b/.test(code);
49
52
  const allPropsArray = Object.entries(args ?? {})
50
- .map(([argKey, argValue]) => argsToProps(argKey, argValue))
53
+ .map(([argKey, argValue]) => {
54
+ // skip children if it's used in the template content (slot)
55
+ if (childrenUsedInTemplate && argKey === 'children') {
56
+ return null;
57
+ }
58
+ return argsToProps(argKey, argValue);
59
+ })
51
60
  .filter((p) => p);
52
61
  let allPropsString = allPropsArray.join(' ');
53
62
  // make the props multiline if the string is longer than 50 chars
@@ -58,6 +67,13 @@ export const generateCodeToEmit = ({ code, args }) => {
58
67
  }
59
68
  let codeToEmit = code
60
69
  .replaceAll('{...args}', allPropsString)
70
+ // replace text expressions that reference a string arg with the plain text,
71
+ // eg. <Button>{args.children}</Button> => <Button>Click me</Button>
72
+ // attribute values like prop={args.something} are skipped
73
+ .replace(/(?<!=)\{\s*(args(?:[\w\d_$.?[\]"'])+)\s*\}/g, (match, argPath) => {
74
+ const value = get({ args }, argPath.replaceAll('?', ''));
75
+ return isPlainText(value) ? value : match;
76
+ })
61
77
  // replace single arg references with their actual value,
62
78
  // eg. myProp={args.something} => myProp={"actual"}
63
79
  // or <h1>{args.something}</h1> => <h1>"actual"</h1>
@@ -68,6 +84,11 @@ export const generateCodeToEmit = ({ code, args }) => {
68
84
  });
69
85
  return codeToEmit;
70
86
  };
87
+ /**
88
+ * A string can be shown as plain text if it has content and
89
+ * no characters that Svelte would read as markup, an expression or an entity
90
+ */
91
+ const isPlainText = (value) => typeof value === 'string' && value.trim() !== '' && !/[{}<>"\\&]/.test(value);
71
92
  const getFunctionName = (fn) => {
72
93
  const name = fn.getMockName?.() ?? fn.name;
73
94
  if (name && name !== 'spy') {
@@ -130,12 +130,12 @@ describe('Emit Code', () => {
130
130
  ternaryProp={true ? "yup" : "nope"}
131
131
  >
132
132
  {args}
133
- {"some text"}
134
- {"optional text"}
133
+ some text
134
+ optional text
135
135
  {{ "nested": "deep" }}
136
136
  {{ "nested": "deep" }}
137
- {"deep"}
138
- {"second"}
137
+ deep
138
+ second
139
139
  {someUnnamedFn}
140
140
  {namedFunc}
141
141
  {vi.fn()}
@@ -146,4 +146,111 @@ describe('Emit Code', () => {
146
146
  </MyComponent>"
147
147
  `);
148
148
  });
149
+ it('should exclude children from props when used in template and unwrap string literals', () => {
150
+ expect(generateCodeToEmit({
151
+ code: '<Button {...args}>{args.children}</Button>',
152
+ args: {
153
+ onclick: 'onclick',
154
+ primary: true,
155
+ children: 'Click me',
156
+ },
157
+ })).toMatchInlineSnapshot(`"<Button onclick="onclick" primary>Click me</Button>"`);
158
+ });
159
+ it('should exclude children from props when children is used in slot', () => {
160
+ expect(generateCodeToEmit({
161
+ code: '<Button {...args}>{args.children}</Button>',
162
+ args: {
163
+ onclick: 'onclick',
164
+ primary: true,
165
+ size: 'large',
166
+ children: 'Button text',
167
+ },
168
+ })).toMatchInlineSnapshot(`"<Button onclick="onclick" primary size="large">Button text</Button>"`);
169
+ });
170
+ it('should handle children when not used in template', () => {
171
+ expect(generateCodeToEmit({
172
+ code: '<Button {...args} />',
173
+ args: {
174
+ onclick: 'onclick',
175
+ children: 'Click me',
176
+ },
177
+ })).toMatchInlineSnapshot(`"<Button onclick="onclick" children="Click me" />"`);
178
+ });
179
+ it('should unwrap string literals in slot content', () => {
180
+ expect(generateCodeToEmit({
181
+ code: '<Component>{args.text}</Component>',
182
+ args: {
183
+ text: 'Hello World',
184
+ },
185
+ })).toMatchInlineSnapshot(`"<Component>Hello World</Component>"`);
186
+ });
187
+ it('should unwrap multiple string literals in slot content', () => {
188
+ expect(generateCodeToEmit({
189
+ code: '<div><p>{args.first}</p><p>{args.second}</p></div>',
190
+ args: {
191
+ first: 'First text',
192
+ second: 'Second text',
193
+ },
194
+ })).toMatchInlineSnapshot(`"<div><p>First text</p><p>Second text</p></div>"`);
195
+ });
196
+ it('should unwrap multiple string args in the same element', () => {
197
+ expect(generateCodeToEmit({
198
+ code: '<p>Hello {args.first} and {args.second}</p>',
199
+ args: {
200
+ first: 'x',
201
+ second: 'y',
202
+ },
203
+ })).toMatchInlineSnapshot(`"<p>Hello x and y</p>"`);
204
+ });
205
+ it('should keep the whitespace around unwrapped string args', () => {
206
+ expect(generateCodeToEmit({
207
+ code: dedent `<Button>
208
+ {args.label}
209
+ </Button>`,
210
+ args: {
211
+ label: 'Hi',
212
+ },
213
+ })).toMatchInlineSnapshot(`
214
+ "<Button>
215
+ Hi
216
+ </Button>"
217
+ `);
218
+ });
219
+ it('should not unwrap strings that Svelte would read as markup or expressions', () => {
220
+ expect(generateCodeToEmit({
221
+ code: dedent `<p>{args.markup}</p>
222
+ <p>{args.braces}</p>
223
+ <p>{args.quotes}</p>
224
+ <p>{args.entity}</p>`,
225
+ args: {
226
+ markup: '<b>bold</b>',
227
+ braces: 'a {b} c',
228
+ quotes: 'say "hi"',
229
+ entity: 'a &amp; b',
230
+ },
231
+ })).toMatchInlineSnapshot(`
232
+ "<p>{"<b>bold</b>"}</p>
233
+ <p>{"a {b} c"}</p>
234
+ <p>{"say \\"hi\\""}</p>
235
+ <p>{"a &amp; b"}</p>"
236
+ `);
237
+ });
238
+ it('should not unwrap non-string args or attribute values', () => {
239
+ expect(generateCodeToEmit({
240
+ code: '<Button label={args.label}>{args.count}</Button>',
241
+ args: {
242
+ label: 'Hi',
243
+ count: 42,
244
+ },
245
+ })).toMatchInlineSnapshot(`"<Button label={"Hi"}>{42}</Button>"`);
246
+ });
247
+ it('should exclude children from props when used with optional chaining', () => {
248
+ expect(generateCodeToEmit({
249
+ code: '<Button {...args}>{args?.children}</Button>',
250
+ args: {
251
+ primary: true,
252
+ children: 'Click me',
253
+ },
254
+ })).toMatchInlineSnapshot(`"<Button primary>Click me</Button>"`);
255
+ });
149
256
  });
package/dist/types.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { ComponentAnnotations as BaseComponentAnnotations, StoryAnnotations as BaseStoryAnnotations, StoryContext as BaseStoryContext, WebRenderer } from 'storybook/internal/types';
2
2
  import type { Component, ComponentProps } from 'svelte';
3
3
  export type Cmp = Component<any>;
4
- export type ComponentAnnotations<TCmp extends Cmp, TArgs extends Record<string, any> = Record<string, any>> = BaseComponentAnnotations<SvelteRenderer<TCmp>, TArgs>;
4
+ export type ComponentAnnotations<TCmp extends Cmp, TArgs extends Record<string, any> = Record<string, any>> = Omit<BaseComponentAnnotations<SvelteRenderer<TCmp>, TArgs>, 'subcomponents'> & {
5
+ subcomponents?: Record<string, Cmp>;
6
+ };
5
7
  export interface SvelteRenderer<TCmp extends Cmp> extends WebRenderer {
6
8
  component: TCmp;
7
9
  storyResult: SvelteStoryResult<TCmp>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/addon-svelte-csf",
3
- "version": "5.1.2",
3
+ "version": "5.1.4",
4
4
  "description": "Allows to write stories in Svelte syntax",
5
5
  "keywords": [
6
6
  "storybook-addons",
@@ -87,9 +87,9 @@
87
87
  "vitest": "^4.1.0"
88
88
  },
89
89
  "peerDependencies": {
90
- "@storybook/svelte": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0",
90
+ "@storybook/svelte": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 || ^11.0.0-0",
91
91
  "@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
92
- "storybook": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0",
92
+ "storybook": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 || ^10.5.0-0 || ^10.6.0-0 || ^11.0.0-0",
93
93
  "svelte": "^5.0.0",
94
94
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
95
95
  },