@storybook/addon-svelte-csf 5.1.3 β 5.1.5
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 +32 -14
- package/dist/compiler/plugins.js +13 -1
- package/dist/compiler/plugins.test.d.ts +1 -0
- package/dist/compiler/plugins.test.js +62 -0
- package/dist/compiler/post-transform/appendix/create-runtime-story-variable-declaration.js +1 -1
- package/dist/compiler/post-transform/define-meta/insert-parameters.js +8 -8
- package/dist/compiler/post-transform/define-meta/insert-parameters.test.d.ts +1 -0
- package/dist/compiler/post-transform/define-meta/insert-parameters.test.js +41 -0
- package/dist/compiler/post-transform/index.test.js +41 -0
- package/dist/compiler/post-transform/story/insert-description.js +8 -8
- package/dist/compiler/post-transform/story/insert-svelte-csf.js +4 -4
- package/dist/compiler/pre-transform/index.d.ts +8 -0
- package/dist/compiler/pre-transform/index.js +12 -0
- package/dist/compiler/pre-transform/index.test.js +15 -0
- package/dist/indexer/parser.js +20 -3
- package/dist/indexer/parser.test.d.ts +1 -0
- package/dist/indexer/parser.test.js +29 -0
- package/dist/parser/analyse/story/content.js +7 -3
- package/dist/parser/analyse/story/content.test.js +29 -0
- package/dist/parser/ast.d.ts +5 -0
- package/dist/parser/ast.js +7 -0
- package/dist/runtime/LegacyStory.svelte +5 -1
- package/dist/runtime/LegacyStory.svelte.d.ts +2 -2
- package/dist/runtime/LegacyTemplate.svelte +5 -5
- package/dist/runtime/LegacyTemplate.svelte.d.ts +1 -1
- package/dist/runtime/emit-code.js +22 -1
- package/dist/runtime/emit-code.test.js +111 -4
- package/dist/types.d.ts +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
# Svelte CSF
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/compiler/plugins.js
CHANGED
|
@@ -35,8 +35,20 @@ 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
|
-
magicCode.overwrite(0, code.length
|
|
51
|
+
magicCode.overwrite(0, code.length, print(transformedSvelteAST));
|
|
40
52
|
const stringifiedMagicCode = magicCode.toString();
|
|
41
53
|
return {
|
|
42
54
|
code: stringifiedMagicCode,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
it('does not repeat the last character of a legacy file', async ({ expect }) => {
|
|
47
|
+
const code = dedent(`
|
|
48
|
+
<script context="module">
|
|
49
|
+
import { Story } from "${pkg.name}";
|
|
50
|
+
import Button from "./Button.svelte";
|
|
51
|
+
|
|
52
|
+
export const meta = { component: Button };
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<Story name="Default" />
|
|
56
|
+
foo
|
|
57
|
+
`);
|
|
58
|
+
const result = await runPreTransform(code);
|
|
59
|
+
expect(code.endsWith('foo')).toBe(true);
|
|
60
|
+
expect(result.code).toMatch(/[^o]foo$/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { STORYBOOK_INTERNAL_PREFIX, SVELTE_CSF_TAG_PREFIX, SVELTE_CSF_V5_TAG, } from '../../../constants.js';
|
|
2
2
|
import { createASTArrayExpression, createASTIdentifier, createASTObjectExpression, createASTProperty, } from '../../../parser/ast.js';
|
|
3
3
|
export function createRuntimeStoryVariableDeclaration(params) {
|
|
4
|
-
const tags = createASTArrayExpression(params.nodes.tags?.elements);
|
|
4
|
+
const tags = createASTArrayExpression([...(params.nodes.tags?.elements ?? [])]);
|
|
5
5
|
// In legacy stories, the pre-transform will add a SVELTE_CSF_V4_TAG tag.
|
|
6
6
|
// if it is not present, we add the SVELTE_CSF_V5_TAG tag.
|
|
7
7
|
const hasSvelteCsfTag = tags.elements.some((element) => element?.type === 'Literal' && element.value?.toString().startsWith(SVELTE_CSF_TAG_PREFIX));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findASTPropertyIndex, findPropertyDescriptionIndex, findPropertyDocsIndex, findPropertyParametersIndex, getDescriptionPropertyValue, getDocsPropertyValue, getParametersPropertyValue, } from '../shared/parameters.js';
|
|
2
|
-
import { createASTObjectExpression, createASTProperty } from '../../../parser/ast.js';
|
|
2
|
+
import { appendASTProperty, createASTObjectExpression, createASTProperty, } from '../../../parser/ast.js';
|
|
3
3
|
import { getDefineMetaFirstArgumentObjectExpression } from '../../../parser/extract/svelte/define-meta.js';
|
|
4
4
|
/**
|
|
5
5
|
* This function inserts parameters to `defineMeta()`.
|
|
@@ -37,25 +37,25 @@ export function insertDefineMetaParameters(params) {
|
|
|
37
37
|
filename,
|
|
38
38
|
node: defineMetaFirstArgumentObjectExpression,
|
|
39
39
|
}) === -1) {
|
|
40
|
-
defineMetaFirstArgumentObjectExpression
|
|
40
|
+
appendASTProperty(defineMetaFirstArgumentObjectExpression, createASTProperty('parameters', createASTObjectExpression()));
|
|
41
41
|
}
|
|
42
42
|
if (findPropertyDocsIndex({
|
|
43
43
|
filename,
|
|
44
44
|
node: defineMetaFirstArgumentObjectExpression,
|
|
45
45
|
}) === -1) {
|
|
46
|
-
getParametersPropertyValue({
|
|
46
|
+
appendASTProperty(getParametersPropertyValue({
|
|
47
47
|
filename,
|
|
48
48
|
node: defineMetaFirstArgumentObjectExpression,
|
|
49
|
-
})
|
|
49
|
+
}), createASTProperty('docs', createASTObjectExpression()));
|
|
50
50
|
}
|
|
51
51
|
if (findPropertyDescriptionIndex({
|
|
52
52
|
filename,
|
|
53
53
|
node: defineMetaFirstArgumentObjectExpression,
|
|
54
54
|
}) === -1) {
|
|
55
|
-
getDocsPropertyValue({
|
|
55
|
+
appendASTProperty(getDocsPropertyValue({
|
|
56
56
|
filename,
|
|
57
57
|
node: defineMetaFirstArgumentObjectExpression,
|
|
58
|
-
})
|
|
58
|
+
}), createASTProperty('description', createASTObjectExpression()));
|
|
59
59
|
}
|
|
60
60
|
if (findASTPropertyIndex({
|
|
61
61
|
name: 'component',
|
|
@@ -70,10 +70,10 @@ export function insertDefineMetaParameters(params) {
|
|
|
70
70
|
${filename}`);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
getDescriptionPropertyValue({
|
|
73
|
+
appendASTProperty(getDescriptionPropertyValue({
|
|
74
74
|
filename,
|
|
75
75
|
node: defineMetaFirstArgumentObjectExpression,
|
|
76
|
-
})
|
|
76
|
+
}), createASTProperty('component', {
|
|
77
77
|
type: 'Literal',
|
|
78
78
|
value: extractDescription(nodes.svelte.defineMetaVariableDeclaration.leadingComments),
|
|
79
79
|
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pkg from '@storybook/addon-svelte-csf/package.json' with { type: 'json' };
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { print } from 'esrap';
|
|
4
|
+
import { parseAst } from 'rollup/parseAst';
|
|
5
|
+
import { compile } from 'svelte/compiler';
|
|
6
|
+
import { describe, it } from 'vitest';
|
|
7
|
+
import { insertDefineMetaParameters } from './insert-parameters.js';
|
|
8
|
+
import { getSvelteAST } from '../../../parser/ast.js';
|
|
9
|
+
import { extractCompiledASTNodes } from '../../../parser/extract/compiled/nodes.js';
|
|
10
|
+
import { extractSvelteASTNodes } from '../../../parser/extract/svelte/nodes.js';
|
|
11
|
+
import { getDefineMetaFirstArgumentObjectExpression } from '../../../parser/extract/svelte/define-meta.js';
|
|
12
|
+
describe(insertDefineMetaParameters.name, () => {
|
|
13
|
+
it('works when defineMeta gets an empty object', async ({ expect }) => {
|
|
14
|
+
const code = dedent `
|
|
15
|
+
<script module>
|
|
16
|
+
import { defineMeta } from "${pkg.name}";
|
|
17
|
+
|
|
18
|
+
/** Description of the component */
|
|
19
|
+
const { Story } = defineMeta({});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<Story name="Default" />
|
|
23
|
+
`;
|
|
24
|
+
const svelteNodes = await extractSvelteASTNodes({ ast: getSvelteAST({ code }) });
|
|
25
|
+
// NOTE: Rollup's parser returns a frozen array for the properties of an empty object literal
|
|
26
|
+
const compiledNodes = await extractCompiledASTNodes({
|
|
27
|
+
ast: parseAst(compile(code, { filename: 'Test.stories.svelte' }).js.code),
|
|
28
|
+
});
|
|
29
|
+
insertDefineMetaParameters({ nodes: { svelte: svelteNodes, compiled: compiledNodes } });
|
|
30
|
+
const objectExpression = getDefineMetaFirstArgumentObjectExpression({ nodes: compiledNodes });
|
|
31
|
+
expect(print(objectExpression).code).toMatchInlineSnapshot(`
|
|
32
|
+
"{
|
|
33
|
+
parameters: {
|
|
34
|
+
docs: {
|
|
35
|
+
description: { component: "Description of the component" }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}"
|
|
39
|
+
`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import url from 'node:url';
|
|
4
|
+
import pkg from '@storybook/addon-svelte-csf/package.json' with { type: 'json' };
|
|
5
|
+
import dedent from 'dedent';
|
|
4
6
|
import MagicString from 'magic-string';
|
|
5
7
|
import { parseAst } from 'rollup/parseAst';
|
|
8
|
+
import { compile } from 'svelte/compiler';
|
|
6
9
|
import { describe, it } from 'vitest';
|
|
7
10
|
import { transformStoriesCode } from './index.js';
|
|
8
11
|
import { getSvelteAST } from '../../parser/ast.js';
|
|
@@ -276,4 +279,42 @@ describe(transformStoriesCode.name, () => {
|
|
|
276
279
|
};"
|
|
277
280
|
`);
|
|
278
281
|
});
|
|
282
|
+
it('works with empty object and array literals', async ({ expect }) => {
|
|
283
|
+
const filename = 'Empty.stories.svelte';
|
|
284
|
+
const originalCode = dedent `
|
|
285
|
+
<script module>
|
|
286
|
+
import { defineMeta } from "${pkg.name}";
|
|
287
|
+
|
|
288
|
+
/** Description of the component */
|
|
289
|
+
const { Story } = defineMeta({});
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
<!-- Description of the story -->
|
|
293
|
+
<Story name="Default" parameters={{}} tags={[]} />
|
|
294
|
+
`;
|
|
295
|
+
const compiledCode = compile(originalCode, { filename, dev: true }).js.code;
|
|
296
|
+
const svelteASTNodes = await extractSvelteASTNodes({
|
|
297
|
+
ast: getSvelteAST({ code: originalCode, filename }),
|
|
298
|
+
filename,
|
|
299
|
+
});
|
|
300
|
+
// NOTE: Rollup's parser returns frozen arrays for empty object and array literals
|
|
301
|
+
const compiledASTNodes = await extractCompiledASTNodes({
|
|
302
|
+
ast: parseAst(compiledCode),
|
|
303
|
+
filename,
|
|
304
|
+
});
|
|
305
|
+
const code = new MagicString(compiledCode);
|
|
306
|
+
await transformStoriesCode({
|
|
307
|
+
code,
|
|
308
|
+
nodes: {
|
|
309
|
+
svelte: svelteASTNodes,
|
|
310
|
+
compiled: compiledASTNodes,
|
|
311
|
+
},
|
|
312
|
+
filename,
|
|
313
|
+
originalCode,
|
|
314
|
+
});
|
|
315
|
+
const output = code.toString();
|
|
316
|
+
expect(output).toContain('component: "Description of the component"');
|
|
317
|
+
expect(output).toContain('story: "Description of the story"');
|
|
318
|
+
expect(output).toContain('__svelteCsf');
|
|
319
|
+
});
|
|
279
320
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { findASTPropertyIndex, findPropertyDescriptionIndex, findPropertyDocsIndex, findPropertyParametersIndex, getParametersPropertyValue, getDocsPropertyValue, getDescriptionPropertyValue, } from '../shared/parameters.js';
|
|
3
|
-
import { createASTObjectExpression, createASTProperty } from '../../../parser/ast.js';
|
|
3
|
+
import { appendASTProperty, createASTObjectExpression, createASTProperty, } from '../../../parser/ast.js';
|
|
4
4
|
import { getStoryPropsObjectExpression } from '../../../parser/extract/compiled/story.js';
|
|
5
5
|
/**
|
|
6
6
|
* Attempt to insert HTML comment above the `<Story />` component as a description **into the compiled code**.
|
|
@@ -22,29 +22,29 @@ export function insertStoryHTMLCommentAsDescription(params) {
|
|
|
22
22
|
component,
|
|
23
23
|
node: storyPropsObjectExpression,
|
|
24
24
|
}) === -1) {
|
|
25
|
-
storyPropsObjectExpression
|
|
25
|
+
appendASTProperty(storyPropsObjectExpression, createASTProperty('parameters', createASTObjectExpression()));
|
|
26
26
|
}
|
|
27
27
|
if (findPropertyDocsIndex({
|
|
28
28
|
filename,
|
|
29
29
|
component,
|
|
30
30
|
node: storyPropsObjectExpression,
|
|
31
31
|
}) === -1) {
|
|
32
|
-
getParametersPropertyValue({
|
|
32
|
+
appendASTProperty(getParametersPropertyValue({
|
|
33
33
|
filename,
|
|
34
34
|
component,
|
|
35
35
|
node: storyPropsObjectExpression,
|
|
36
|
-
})
|
|
36
|
+
}), createASTProperty('docs', createASTObjectExpression()));
|
|
37
37
|
}
|
|
38
38
|
if (findPropertyDescriptionIndex({
|
|
39
39
|
filename,
|
|
40
40
|
component,
|
|
41
41
|
node: storyPropsObjectExpression,
|
|
42
42
|
}) === -1) {
|
|
43
|
-
getDocsPropertyValue({
|
|
43
|
+
appendASTProperty(getDocsPropertyValue({
|
|
44
44
|
filename,
|
|
45
45
|
component,
|
|
46
46
|
node: storyPropsObjectExpression,
|
|
47
|
-
})
|
|
47
|
+
}), createASTProperty('description', createASTObjectExpression()));
|
|
48
48
|
}
|
|
49
49
|
if (findASTPropertyIndex({
|
|
50
50
|
filename,
|
|
@@ -67,11 +67,11 @@ export function insertStoryHTMLCommentAsDescription(params) {
|
|
|
67
67
|
${filename}`);
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
getDescriptionPropertyValue({
|
|
70
|
+
appendASTProperty(getDescriptionPropertyValue({
|
|
71
71
|
filename,
|
|
72
72
|
component,
|
|
73
73
|
node: storyPropsObjectExpression,
|
|
74
|
-
})
|
|
74
|
+
}), createASTProperty('story', {
|
|
75
75
|
type: 'Literal',
|
|
76
76
|
value: dedent(comment.data),
|
|
77
77
|
}));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { findPropertyParametersIndex, getParametersPropertyValue, } from '../shared/parameters.js';
|
|
2
2
|
import { getStoryPropsObjectExpression } from '../../../parser/extract/compiled/story.js';
|
|
3
3
|
import { getStoryContentRawCode } from '../../../parser/analyse/story/content.js';
|
|
4
|
-
import { createASTObjectExpression, createASTProperty } from '../../../parser/ast.js';
|
|
4
|
+
import { appendASTProperty, createASTObjectExpression, createASTProperty, } from '../../../parser/ast.js';
|
|
5
5
|
/**
|
|
6
6
|
* Insert addon's internal object `__svelteCsf`
|
|
7
7
|
* to `parameters` of every `<Story />` component **into the compiled code**.
|
|
@@ -18,7 +18,7 @@ export function insertSvelteCSFToStoryParameters(params) {
|
|
|
18
18
|
component: component.svelte.component,
|
|
19
19
|
node: storyPropsObjectExpression,
|
|
20
20
|
}) === -1) {
|
|
21
|
-
storyPropsObjectExpression
|
|
21
|
+
appendASTProperty(storyPropsObjectExpression, createASTProperty('parameters', createASTObjectExpression()));
|
|
22
22
|
}
|
|
23
23
|
const rawCode = getStoryContentRawCode({
|
|
24
24
|
nodes: {
|
|
@@ -27,11 +27,11 @@ export function insertSvelteCSFToStoryParameters(params) {
|
|
|
27
27
|
},
|
|
28
28
|
originalCode,
|
|
29
29
|
});
|
|
30
|
-
getParametersPropertyValue({
|
|
30
|
+
appendASTProperty(getParametersPropertyValue({
|
|
31
31
|
filename,
|
|
32
32
|
component: component.svelte.component,
|
|
33
33
|
node: storyPropsObjectExpression,
|
|
34
|
-
})
|
|
34
|
+
}), createASTProperty('__svelteCsf', createASTObjectExpression([
|
|
35
35
|
createASTProperty('rawCode', {
|
|
36
36
|
type: 'Literal',
|
|
37
37
|
value: rawCode,
|
|
@@ -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
|
});
|
package/dist/indexer/parser.js
CHANGED
|
@@ -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 },
|
|
32
|
+
let [code, { walk }, svelteConfig] = await Promise.all([
|
|
15
33
|
fs.readFile(filename, { encoding: 'utf8' }),
|
|
16
34
|
import('zimmerframe'),
|
|
17
|
-
|
|
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
|
-
//
|
|
127
|
-
|
|
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
|
|
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
|
});
|
package/dist/parser/ast.d.ts
CHANGED
|
@@ -29,6 +29,11 @@ export declare function createASTArrayExpression(elements?: ESTreeAST.ArrayExpre
|
|
|
29
29
|
* By default it will create an empty object.
|
|
30
30
|
*/
|
|
31
31
|
export declare function createASTObjectExpression(properties?: ESTreeAST.ObjectExpression['properties']): ESTreeAST.ObjectExpression;
|
|
32
|
+
/**
|
|
33
|
+
* Append a property to an {@link ESTreeAST.ObjectExpression}.
|
|
34
|
+
* It replaces the array instead of using `push`, because Rollup's parser returns a frozen array for an empty object literal.
|
|
35
|
+
*/
|
|
36
|
+
export declare function appendASTProperty(node: ESTreeAST.ObjectExpression, property: ESTreeAST.ObjectExpression['properties'][number]): void;
|
|
32
37
|
interface ASTScriptOptions {
|
|
33
38
|
module?: boolean;
|
|
34
39
|
content: SvelteAST.Script['content'];
|
package/dist/parser/ast.js
CHANGED
|
@@ -66,6 +66,13 @@ export function createASTObjectExpression(properties = []) {
|
|
|
66
66
|
properties,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Append a property to an {@link ESTreeAST.ObjectExpression}.
|
|
71
|
+
* It replaces the array instead of using `push`, because Rollup's parser returns a frozen array for an empty object literal.
|
|
72
|
+
*/
|
|
73
|
+
export function appendASTProperty(node, property) {
|
|
74
|
+
node.properties = [...node.properties, property];
|
|
75
|
+
}
|
|
69
76
|
export function createASTScript(options) {
|
|
70
77
|
const { content, module = false } = options;
|
|
71
78
|
const attributes = [];
|
|
@@ -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
|
-
|
|
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, {},
|
|
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
|
-
|
|
15
|
-
//
|
|
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, {},
|
|
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]) =>
|
|
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
|
-
|
|
134
|
-
|
|
133
|
+
some text
|
|
134
|
+
optional text
|
|
135
135
|
{{ "nested": "deep" }}
|
|
136
136
|
{{ "nested": "deep" }}
|
|
137
|
-
|
|
138
|
-
|
|
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 & b',
|
|
230
|
+
},
|
|
231
|
+
})).toMatchInlineSnapshot(`
|
|
232
|
+
"<p>{"<b>bold</b>"}</p>
|
|
233
|
+
<p>{"a {b} c"}</p>
|
|
234
|
+
<p>{"say \\"hi\\""}</p>
|
|
235
|
+
<p>{"a & 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>;
|