diff --git a/code/renderers/svelte/src/__test__/svelte-csf-factories/meta-type.stories.svelte b/code/renderers/svelte/src/__test__/svelte-csf-factories/meta-type.stories.svelte
new file mode 100644
index 000000000000..a987d558d0e4
--- /dev/null
+++ b/code/renderers/svelte/src/__test__/svelte-csf-factories/meta-type.stories.svelte
@@ -0,0 +1,32 @@
+
+
+ {
+ await expect(canvas.getByRole('button')).toHaveTextContent('Primary');
+ }}
+/>
+
+ {
+ await expect(args.icon).toBe('★');
+ await expect(canvas.getByRole('button')).toHaveTextContent('★');
+ }}
+>
+ {#snippet template({ icon, ...args })}
+
+ {/snippet}
+
diff --git a/code/renderers/svelte/src/__test__/svelte-csf-factories/svelte-csf-factories.test.ts b/code/renderers/svelte/src/__test__/svelte-csf-factories/svelte-csf-factories.test.ts
index 299c48aaccb2..c727198f57d4 100644
--- a/code/renderers/svelte/src/__test__/svelte-csf-factories/svelte-csf-factories.test.ts
+++ b/code/renderers/svelte/src/__test__/svelte-csf-factories/svelte-csf-factories.test.ts
@@ -6,13 +6,14 @@ import { isStory } from 'storybook/internal/csf';
import { composeStories } from '../../portable-stories.ts';
import * as ButtonStories from './button.stories.svelte';
import * as InteractionsStories from './interactions.stories.svelte';
+import * as MetaTypeStories from './meta-type.stories.svelte';
import * as TemplatingStories from './templating.stories.svelte';
afterEach(() => {
cleanup();
});
-const files = { ButtonStories, InteractionsStories, TemplatingStories };
+const files = { ButtonStories, InteractionsStories, MetaTypeStories, TemplatingStories };
describe.each(Object.entries(files))('%s', (_name, file) => {
const stories = Object.entries(file).filter(([, story]) => isStory(story));
@@ -26,6 +27,13 @@ describe.each(Object.entries(files))('%s', (_name, file) => {
});
});
+it('exports the stories of the Story components from meta.type<>()', () => {
+ expect(Object.keys(MetaTypeStories).filter((key) => key !== '__namedExportsOrder')).toEqual([
+ 'Primary',
+ 'Icon',
+ ]);
+});
+
it('composes the stories with composeStories', async () => {
// The types of composeStories need a default export, which a CSF factories file doesn't have.
const { Primary } = composeStories(ButtonStories as never) as Record;
diff --git a/code/renderers/svelte/src/__test__/svelte-csf-factories/type-checks-meta-type.svelte b/code/renderers/svelte/src/__test__/svelte-csf-factories/type-checks-meta-type.svelte
new file mode 100644
index 000000000000..ecb8bc641d92
--- /dev/null
+++ b/code/renderers/svelte/src/__test__/svelte-csf-factories/type-checks-meta-type.svelte
@@ -0,0 +1,126 @@
+
+
+
+
+ {
+ expectTypeOf(args.icon).toEqualTypeOf<'star' | 'heart'>();
+ }}
+>
+ {#snippet template(args)}
+ {expectTypeOf(args.icon).toEqualTypeOf<'star' | 'heart'>()}
+ {expectTypeOf(args.label).toEqualTypeOf()}
+
+ {/snippet}
+
+
+
+ {#snippet template(args)}
+ {expectTypeOf(args).not.toHaveProperty('icon')}
+
+ {/snippet}
+
+
+
+
+{#snippet iconTemplate(args: Args)}
+ {expectTypeOf(args.icon).toEqualTypeOf<'star' | 'heart'>()}
+
+{/snippet}
+
+
+
+
+
+{/* @ts-expect-error icon is required */ null}
+
+
+
+
+
+
+
+
+
+
+
+
+{/* @ts-expect-error label is required */ null}
+
+
+
+
+{/* @ts-expect-error disabled is required, the meta sets it to false */ null}
+
+
+
+
+
+
+
+
+{/* @ts-expect-error icon is required */ null}
+
+
+
+ Static content
+
+
+
+ {#snippet template()}
+ Static content
+ {/snippet}
+
+
+
+
+ {
+ expectTypeOf(args.locale).toEqualTypeOf<'en' | 'nl'>();
+ expectTypeOf(args.theme).toEqualTypeOf<'light' | 'dark'>();
+ expectTypeOf(args.icon).toEqualTypeOf();
+ expectTypeOf(args.size).toEqualTypeOf();
+ }}
+/>
+
+{/* @ts-expect-error size is required */ null}
+
diff --git a/code/renderers/svelte/src/svelte-csf/compiler/post-transform/index.test.ts b/code/renderers/svelte/src/svelte-csf/compiler/post-transform/index.test.ts
index fe3c0325ec94..55ca23424704 100644
--- a/code/renderers/svelte/src/svelte-csf/compiler/post-transform/index.test.ts
+++ b/code/renderers/svelte/src/svelte-csf/compiler/post-transform/index.test.ts
@@ -486,11 +486,187 @@ describe('CSF factories', () => {
`);
- expect(output).toContain(dedent`
+ expect(output).toMatchInlineSnapshot(`
+ "import 'svelte/internal/disclose-version';
+ import 'svelte/internal/flags/legacy';
+
+ Button_stories[$.FILENAME] = 'Button.stories.svelte';
+
+ import preview from '#.storybook/preview';
+ import Button from './Button.svelte';
+ import * as $ from 'svelte/internal/client';
+
const $__meta = preview.type().meta({ component: Button });
const { Story } = $__meta;
+
+ function Button_stories($$anchor, $$props) {
+ $.check_target(new.target);
+ $.push($$props, false, Button_stories);
+
+ var $$exports = { ...$.legacy_api() };
+
+ $.init();
+
+ $.add_svelte_meta(
+ () => Story($$anchor, {
+ name: 'Primary',
+ args: { label: 'Primary', theme: 'dark' },
+ parameters: {
+ __svelteCsf: { rawCode: "" }
+ }
+ }),
+ 'component',
+ Button_stories,
+ 8,
+ 0,
+ { componentTag: 'Story' }
+ );
+
+ return $.pop($$exports);
+ }
+ import { createRuntimeStories } from "@storybook/svelte/internal/svelte-csf/create-runtime-stories";
+
+ const $__stories = createRuntimeStories(Button_stories, $__meta.input);
+
+ export const __namedExportsOrder = ["Primary"];
+
+ const $__Primary = $__meta.story({
+ ...$__stories["Primary"],
+ tags: ["svelte-csf-v5"]
+ });
+
+ export { $__Primary as Primary };"
+ `);
+ });
+
+ it('creates the stories of the Story components from meta.type<>()', async ({ expect }) => {
+ const output = await transform(dedent`
+
+
+
+
+
+ {#snippet template({ icon, ...args })}
+
+ {/snippet}
+
+ `);
+
+ expect(output).toMatchInlineSnapshot(`
+ "import 'svelte/internal/disclose-version';
+ import 'svelte/internal/flags/legacy';
+
+ Button_stories[$.FILENAME] = 'Button.stories.svelte';
+
+ import preview from '#.storybook/preview';
+ import Button from './Button.svelte';
+ import * as $ from 'svelte/internal/client';
+
+ const meta = preview.meta({ component: Button });
+ const { Story } = meta;
+ const { Story: IconStory } = meta.type();
+ var root = $.add_locations($.from_html(\` \`, 1), Button_stories[$.FILENAME], []);
+
+ function Button_stories($$anchor, $$props) {
+ $.check_target(new.target);
+ $.push($$props, false, Button_stories);
+
+ var $$exports = { ...$.legacy_api() };
+
+ $.init();
+
+ var fragment = root();
+ var node = $.first_child(fragment);
+
+ $.add_svelte_meta(
+ () => Story(node, {
+ name: 'Primary',
+ args: { label: 'Primary' },
+ parameters: {
+ __svelteCsf: { rawCode: "" }
+ }
+ }),
+ 'component',
+ Button_stories,
+ 10,
+ 0,
+ { componentTag: 'Story' }
+ );
+
+ var node_1 = $.sibling(node, 2);
+
+ {
+ const template = $.wrap_snippet(Button_stories, function ($$anchor, $$arg0) {
+ $.validate_snippet_args(...arguments);
+
+ let icon = () => $$arg0?.().icon;
+
+ icon();
+
+ let args = () => $.exclude_from_object($$arg0?.(), ['icon']);
+
+ args();
+
+ $.add_svelte_meta(
+ () => Button($$anchor, $.spread_props(args, {
+ get label() {
+ return icon();
+ }
+ })),
+ 'component',
+ Button_stories,
+ 14,
+ 4,
+ { componentTag: 'Button' }
+ );
+ });
+
+ $.add_svelte_meta(
+ () => IconStory(node_1, {
+ name: 'Icon',
+ args: { label: 'Icon', icon: 'star' },
+ template,
+ $$slots: { template: true },
+ parameters: {
+ __svelteCsf: { rawCode: "" }
+ }
+ }),
+ 'component',
+ Button_stories,
+ 12,
+ 0,
+ { componentTag: 'IconStory' }
+ );
+ }
+
+ $.append($$anchor, fragment);
+ return $.pop($$exports);
+ }
+ import { createRuntimeStories } from "@storybook/svelte/internal/svelte-csf/create-runtime-stories";
+
+ const $__stories = createRuntimeStories(Button_stories, meta.input);
+
+ export const __namedExportsOrder = ["Primary", "Icon"];
+
+ const $__Primary = meta.story({
+ ...$__stories["Primary"],
+ tags: ["svelte-csf-v5"]
+ });
+
+ const $__Icon = meta.story({
+ ...$__stories["Icon"],
+ tags: ["svelte-csf-v5"]
+ });
+
+ export { $__Primary as Primary, $__Icon as Icon };"
`);
- expect(output).toContain('const $__Primary = $__meta.story({');
});
it('uses the meta variable of the file', async ({ expect }) => {
diff --git a/code/renderers/svelte/src/svelte-csf/compiler/vitest-transform.test.ts b/code/renderers/svelte/src/svelte-csf/compiler/vitest-transform.test.ts
index 01bf77ebd5a9..2712fc9a6b72 100644
--- a/code/renderers/svelte/src/svelte-csf/compiler/vitest-transform.test.ts
+++ b/code/renderers/svelte/src/svelte-csf/compiler/vitest-transform.test.ts
@@ -93,4 +93,67 @@ describe('vitestTransform of a CSF factories stories file', () => {
}"
`);
});
+
+ it('tests the stories of the Story components from meta.type<>()', async ({ expect }) => {
+ const output = await transform(dedent`
+
+
+
+
+
+ `);
+
+ expect(output.slice(output.indexOf('const _isRunningFromThisFile'))).toMatchInlineSnapshot(`
+ "const _isRunningFromThisFile = convertToFilePath(import.meta.url).includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath);
+ if (_isRunningFromThisFile) {
+ _test("Primary", _testStory({
+ exportName: "Primary",
+ story: $__Primary,
+ meta: meta,
+ skipTags: [],
+ storyId: "button--primary",
+ componentPath: "./Button.svelte",
+ componentName: "Button"
+ }));
+ _test("Icon", _testStory({
+ exportName: "Icon",
+ story: $__Icon,
+ meta: meta,
+ skipTags: [],
+ storyId: "button--icon",
+ componentPath: "./Button.svelte",
+ componentName: "Button"
+ }));
+ }"
+ `);
+ });
+
+ it('tests the Story components declared together with Story', async ({ expect }) => {
+ const output = await transform(dedent`
+
+
+
+
+
+ `);
+
+ expect([...output.matchAll(/_test\("(\w+)"/g)].map(([, name]) => name)).toEqual([
+ 'Primary',
+ 'Icon',
+ ]);
+ });
});
diff --git a/code/renderers/svelte/src/svelte-csf/indexer/parser.test.ts b/code/renderers/svelte/src/svelte-csf/indexer/parser.test.ts
index 7d85280db790..3d589f2027b4 100644
--- a/code/renderers/svelte/src/svelte-csf/indexer/parser.test.ts
+++ b/code/renderers/svelte/src/svelte-csf/indexer/parser.test.ts
@@ -246,6 +246,65 @@ describe('parseForIndexer', () => {
`);
});
+ it('indexes the stories of the Story components from meta.type<>()', async ({ expect }) => {
+ const file = await writeStoriesFile(`
+
+
+
+
+ `);
+
+ expect(await parseForIndexer(file)).toMatchInlineSnapshot(`
+ {
+ "meta": {
+ "tags": [
+ "meta-tag",
+ ],
+ "title": "Example",
+ },
+ "stories": [
+ {
+ "exportName": "Primary",
+ "name": "Primary",
+ "tags": [
+ "autodocs",
+ ],
+ },
+ {
+ "exportName": "Icon",
+ "name": "Icon",
+ "tags": [
+ "icon-tag",
+ ],
+ },
+ ],
+ }
+ `);
+ });
+
+ it('indexes the Story components declared together with Story', async ({ expect }) => {
+ const file = await writeStoriesFile(`
+
+
+
+
+ `);
+
+ const { stories } = await parseForIndexer(file);
+
+ expect(stories.map((story) => story.exportName)).toEqual(['Primary', 'Icon']);
+ });
+
it('fails when a file calls defineMeta() and preview.meta()', async ({ expect }) => {
const file = await writeFactoryStoriesFile(`
import { defineMeta } from '@storybook/svelte';
diff --git a/code/renderers/svelte/src/svelte-csf/indexer/parser.ts b/code/renderers/svelte/src/svelte-csf/indexer/parser.ts
index f465397d3a73..f80f8bc05f8e 100644
--- a/code/renderers/svelte/src/svelte-csf/indexer/parser.ts
+++ b/code/renderers/svelte/src/svelte-csf/indexer/parser.ts
@@ -57,10 +57,11 @@ export async function parseForIndexer(filename: string): Promise {
const svelteAST = getIndexableAST(code, filename);
const results: Results & {
hasMetaImport?: boolean;
- defineMetaStory?: ESTreeAST.Identifier;
+ storyNames: string[];
} = {
meta: {},
stories: [],
+ storyNames: [],
};
let hasDefaultOrNamespaceImport = false;
@@ -112,14 +113,14 @@ export async function parseForIndexer(filename: string): Promise {
});
}
- if (!meta.storyIdentifier) {
+ if (meta.storyNames.length === 0) {
throw new NoStoryComponentDestructuredError({
filename,
metaFunctionName: meta.functionName,
});
}
- state.defineMetaStory = meta.storyIdentifier;
+ state.storyNames = meta.storyNames;
if (meta.call.arguments[0]?.type !== 'ObjectExpression') {
throw new GetDefineMetaFirstArgumentError({
@@ -181,7 +182,7 @@ export async function parseForIndexer(filename: string): Promise {
const { name } = node;
const { state } = context;
- if (state.defineMetaStory?.name === name) {
+ if (state.storyNames.includes(name)) {
const storyAttributes = extractStoryAttributesNodes({
component: node,
attributes: ['exportName', 'name', 'tags', 'template', 'asChild', 'children', 'play'],
diff --git a/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.test.ts b/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.test.ts
index b6a8652c2b2a..322ed5892873 100644
--- a/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.test.ts
+++ b/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.test.ts
@@ -81,7 +81,7 @@ describe(extractCompiledASTNodes.name, () => {
expect(nodes.isFactory).toBe(true);
expect(nodes.metaIdentifier).toBeUndefined();
- expect(nodes.storyIdentifier.name).toBe('Story');
+ expect(nodes.storyNames).toEqual(['Story']);
});
it('finds the meta variable of preview.meta()', async ({ expect }) => {
@@ -95,6 +95,20 @@ describe(extractCompiledASTNodes.name, () => {
expect(nodes.isFactory).toBe(true);
expect(nodes.metaIdentifier?.name).toBe('meta');
- expect(nodes.storyIdentifier.name).toBe('S');
+ expect(nodes.storyNames).toEqual(['S']);
+ });
+
+ it('finds the Story components of meta.type()', async ({ expect }) => {
+ const ast = getCompiledAST(`
+ import preview from '#.storybook/preview';
+ const meta = preview.type().meta({});
+ const { Story } = meta;
+ const { Story: IconStory } = meta.type();
+ const { Story: SizeStory } = meta.type().type();
+ `);
+
+ const nodes = await extractCompiledASTNodes({ ast });
+
+ expect(nodes.storyNames).toEqual(['Story', 'IconStory', 'SizeStory']);
});
});
diff --git a/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.ts b/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.ts
index 8af92d12f8ea..d697a7b013af 100644
--- a/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.ts
+++ b/code/renderers/svelte/src/svelte-csf/parser/extract/compiled/nodes.ts
@@ -34,10 +34,10 @@ export interface CompiledASTNodes {
*/
exportDefault: ESTreeAST.ExportDefaultDeclaration;
/**
- * An identifier for the `` component.
+ * The names of the `` components.
* It could be destructured with rename - e.g. `const { Story: S } = defineMeta({ ... })`
*/
- storyIdentifier: ESTreeAST.Identifier;
+ storyNames: string[];
/**
* A function declaration for the main Svelte component which is the `*.stories.svelte` file.
*/
@@ -113,7 +113,7 @@ export async function extractCompiledASTNodes(params: Params): Promise = {
CallExpression(node, context) {
const { state } = context;
- if (node.callee.type === 'Identifier' && node.callee.name === storyIdentifier.name) {
+ if (node.callee.type === 'Identifier' && storyNames.includes(node.callee.name)) {
state.push(node);
} else {
context.next();
diff --git a/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/fragment-nodes.ts b/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/fragment-nodes.ts
index 2de374b56fbb..241f26f0c3be 100644
--- a/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/fragment-nodes.ts
+++ b/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/fragment-nodes.ts
@@ -36,7 +36,7 @@ export async function extractFragmentNodes(params: Params): Promise {
const { walk } = await import('zimmerframe');
const { fragment, moduleNodes } = params;
- const { storyIdentifier } = moduleNodes;
+ const { storyNames } = moduleNodes;
let latestComment: SvelteAST.Comment | undefined;
@@ -52,7 +52,7 @@ export async function extractFragmentNodes(params: Params): Promise {
},
Component(node, { state }) {
- if (node.name === storyIdentifier.name) {
+ if (storyNames.includes(node.name)) {
state.storyComponents.push({
comment: latestComment,
component: node,
diff --git a/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.test.ts b/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.test.ts
index 35264d6bd408..4a090ca4c7d0 100644
--- a/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.test.ts
+++ b/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.test.ts
@@ -153,8 +153,7 @@ describe(extractModuleNodes.name, () => {
expect(nodes.defineMetaVariableDeclaration.declarations[0].init).toMatchObject({
callee: { name: 'defineMeta' },
});
- expect(nodes.storyIdentifier).toBeDefined();
- expect(nodes.storyIdentifier.name).toBe('Story');
+ expect(nodes.storyNames).toEqual(['Story']);
});
it('extracts module nodes with renamed identifiers', async ({ expect }) => {
@@ -173,7 +172,7 @@ describe(extractModuleNodes.name, () => {
callee: { name: 'dm' },
});
expect(nodes.defineMetaVariableDeclaration).toBeDefined();
- expect(nodes.storyIdentifier.name).toBe('S');
+ expect(nodes.storyNames).toEqual(['S']);
});
it.for(SVELTE_CSF_IMPORT_SOURCES)(
@@ -290,7 +289,7 @@ describe(extractModuleNodes.name, () => {
expect(nodes.isFactory).toBe(true);
expect(nodes.metaIdentifier).toBeUndefined();
- expect(nodes.storyIdentifier.name).toBe('Story');
+ expect(nodes.storyNames).toEqual(['Story']);
});
it('extracts the nodes of a meta variable', async ({ expect }) => {
@@ -308,7 +307,60 @@ describe(extractModuleNodes.name, () => {
expect(nodes.isFactory).toBe(true);
expect(nodes.metaIdentifier?.name).toBe('meta');
- expect(nodes.storyIdentifier.name).toBe('S');
+ expect(nodes.storyNames).toEqual(['S']);
+ });
+
+ it('extracts the Story components of meta.type<>()', async ({ expect }) => {
+ const { module } = getSvelteAST({
+ code: `
+
+ `,
+ });
+
+ const nodes = await extractModuleNodes({ module });
+
+ expect(nodes.storyNames).toEqual(['Story', 'IconStory', 'SizeStory']);
+ });
+
+ it('extracts the nodes of preview.type<>().meta()', async ({ expect }) => {
+ const { module } = getSvelteAST({
+ code: `
+
+ `,
+ });
+
+ const nodes = await extractModuleNodes({ module });
+
+ expect(nodes.isFactory).toBe(true);
+ expect(nodes.storyNames).toEqual(['Story']);
+ });
+
+ it('extracts a Story component of meta.type<>() alone', async ({ expect }) => {
+ const { module } = getSvelteAST({
+ code: `
+
+ `,
+ });
+
+ const nodes = await extractModuleNodes({ module });
+
+ expect(nodes.storyNames).toEqual(['IconStory']);
});
it('fails when Story is not destructured from the meta', async ({ expect }) => {
@@ -420,7 +472,7 @@ describe(extractModuleNodes.name, () => {
const nodes = await extractModuleNodes({ module });
- expect(nodes.storyIdentifier.name).toBe('Story');
+ expect(nodes.storyNames).toEqual(['Story']);
});
});
});
diff --git a/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.ts b/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.ts
index 997f0afb8ee9..2b8f2e702a0e 100644
--- a/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.ts
+++ b/code/renderers/svelte/src/svelte-csf/parser/extract/svelte/module-nodes.ts
@@ -21,10 +21,10 @@ interface Result {
/** `meta` in `const meta = preview.meta({ })`. */
metaIdentifier?: ESTreeAST.Identifier;
/**
- * An identifier for the `` component.
+ * The names of the `` components.
* It could be destructured with rename - e.g. `const { Story: S } = defineMeta({ ... })`
*/
- storyIdentifier: ESTreeAST.Identifier;
+ storyNames: string[];
}
interface Params {
@@ -59,7 +59,7 @@ export async function extractModuleNodes(options: Params): Promise {
throw new MissingDefineMetaVariableDeclarationError(filename);
}
- if (!meta.storyIdentifier) {
+ if (meta.storyNames.length === 0) {
throw new NoStoryComponentDestructuredError({
filename,
metaFunctionName: meta.functionName,
@@ -70,6 +70,6 @@ export async function extractModuleNodes(options: Params): Promise {
isFactory: meta.isFactory,
defineMetaVariableDeclaration: meta.declaration,
metaIdentifier: meta.metaIdentifier,
- storyIdentifier: meta.storyIdentifier,
+ storyNames: meta.storyNames,
};
}
diff --git a/code/renderers/svelte/src/svelte-csf/utils/import-source.ts b/code/renderers/svelte/src/svelte-csf/utils/import-source.ts
index 796777353031..18bb577144c2 100644
--- a/code/renderers/svelte/src/svelte-csf/utils/import-source.ts
+++ b/code/renderers/svelte/src/svelte-csf/utils/import-source.ts
@@ -58,7 +58,8 @@ export function hasMetaImport(imports: MetaImports): boolean {
}
// `const { Story } = defineMeta({ … })`, `const { Story } = preview.meta({ … })`, or
-// `const meta = preview.meta({ … })` followed by `const { Story } = meta`
+// `const meta = preview.meta({ … })` followed by `const { Story } = meta` and
+// `const { Story: IconStory } = meta.type<…>()`
export interface MetaNodes {
// `preview.meta()` creates a CSF factories meta
isFactory: boolean;
@@ -68,8 +69,8 @@ export interface MetaNodes {
functionName: string;
// `meta` in `const meta = preview.meta({ … })`
metaIdentifier?: ESTreeAST.Identifier;
- // It can be renamed: `const { Story: S } = …`
- storyIdentifier?: ESTreeAST.Identifier;
+ // The names of the `Story` components. They can be renamed: `const { Story: S } = …`
+ storyNames: string[];
}
export function findMeta(
@@ -106,14 +107,19 @@ export function findMeta(
call: init,
functionName,
metaIdentifier: isFactory && id.type === 'Identifier' ? id : undefined,
- storyIdentifier: findStoryIdentifier(id),
+ storyNames: findStoryNames(id),
};
- } else if (
- meta?.metaIdentifier &&
- init?.type === 'Identifier' &&
- init.name === meta.metaIdentifier.name
- ) {
- meta.storyIdentifier ??= findStoryIdentifier(id);
+ } else if (meta?.metaIdentifier) {
+ for (const declarator of statement.declarations) {
+ const metaReference = declarator.init && skipTypeCalls(declarator.init);
+
+ if (
+ metaReference?.type === 'Identifier' &&
+ metaReference.name === meta.metaIdentifier.name
+ ) {
+ meta.storyNames.push(...findStoryNames(declarator.id));
+ }
+ }
}
}
@@ -141,19 +147,7 @@ function getMetaFunctionName(
return undefined;
}
- let { object } = callee;
-
- // `preview.type<…>()` returns the preview with more types, so its `.meta()` is a meta of `preview`
- if (
- object.type === 'CallExpression' &&
- object.arguments.length === 0 &&
- object.callee.type === 'MemberExpression' &&
- !object.callee.computed &&
- object.callee.property.type === 'Identifier' &&
- object.callee.property.name === 'type'
- ) {
- object = object.callee.object;
- }
+ const object = skipTypeCalls(callee.object);
if (object.type !== 'Identifier') {
return undefined;
@@ -172,21 +166,36 @@ function getMetaFunctionName(
return undefined;
}
-function findStoryIdentifier(id: ESTreeAST.Pattern): ESTreeAST.Identifier | undefined {
- if (id.type !== 'ObjectPattern') {
- return undefined;
+// `preview.type<…>()` and `meta.type<…>()` return the same preview or meta with more types, so
+// `preview.type<…>().meta()` is a meta of `preview`, and `meta.type<…>()` is `meta`
+function skipTypeCalls(expression: ESTreeAST.Expression | ESTreeAST.Super) {
+ let current = expression;
+
+ while (
+ current.type === 'CallExpression' &&
+ current.arguments.length === 0 &&
+ current.callee.type === 'MemberExpression' &&
+ !current.callee.computed &&
+ current.callee.property.type === 'Identifier' &&
+ current.callee.property.name === 'type'
+ ) {
+ current = current.callee.object;
}
- for (const property of id.properties) {
- if (
- property.type === 'Property' &&
- property.key.type === 'Identifier' &&
- property.key.name === 'Story' &&
- property.value.type === 'Identifier'
- ) {
- return property.value;
- }
+ return current;
+}
+
+function findStoryNames(id: ESTreeAST.Pattern): string[] {
+ if (id.type !== 'ObjectPattern') {
+ return [];
}
- return undefined;
+ return id.properties.flatMap((property) =>
+ property.type === 'Property' &&
+ property.key.type === 'Identifier' &&
+ property.key.name === 'Story' &&
+ property.value.type === 'Identifier'
+ ? [property.value.name]
+ : []
+ );
}