Skip to content

Commit

Permalink
Allow 'meta' to be exported as const from module script
Browse files Browse the repository at this point in the history
  • Loading branch information
j3rem1e committed Sep 14, 2023
1 parent 04308e5 commit 74c982c
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 47 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ It supports:
## Example

```svelte
<script>
import { Meta, Story, Template } from '@storybook/addon-svelte-csf';
<script content="module">
import Button from './Button.svelte';
export const meta = {
title: "Button",
component: Button
}
</script>
<script>
import { Story, Template } from '@storybook/addon-svelte-csf';
let count = 0;
function handleClick() {
count += 1;
}
</script>
<Meta title="Button" component={Button}/>
<Template let:args>
<Button {...args} on:click={handleClick}>
You clicked: {count}
Expand Down
5 changes: 2 additions & 3 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ interface MetaProps extends BaseMeta<any>, BaseAnnotations<any, DecoratorReturnT
*
* It should be a static array of strings.
*
* @example
* tags={['autodocs']}
* @example tags={['autodocs']}
*/
tags?: string[];
}
Expand All @@ -72,7 +71,7 @@ interface Slots {
/**
* Meta.
*
* @deprecated See https://github.com/storybookjs/addon-svelte-csf/issues/135
* @deprecated Use `export const meta`. See https://github.com/storybookjs/addon-svelte-csf for an example
*/
export class Meta extends SvelteComponent<MetaProps> { }
/**
Expand Down
4 changes: 2 additions & 2 deletions src/parser/collect-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const createFragment = document.createDocumentFragment
? () => document.createDocumentFragment()
: () => document.createElement('div');

export default (StoriesComponent, { stories = {}, allocatedIds = [] }) => {
export default (StoriesComponent, { stories = {}, allocatedIds = [] }, exportedMeta = undefined) => {
const repositories = {
meta: null as Meta | null,
stories: [] as Story[],
Expand All @@ -55,7 +55,7 @@ export default (StoriesComponent, { stories = {}, allocatedIds = [] }) => {
logger.error(`Error extracting stories ${e.toString()}`, e);
}

const { meta } = repositories;
const meta = exportedMeta || repositories.meta;
if (!meta) {
logger.error('Missing <Meta/> tag');
return {};
Expand Down
26 changes: 26 additions & 0 deletions src/parser/extract-stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,32 @@ describe('extractSource', () => {
}
`);
});
test('Meta as exported module object', () => {
expect(
extractStories(`
<script context='module'>
export const meta = {
title: 'MyStory',
tags: ['a']
};
</script>
`)
).toMatchInlineSnapshot(`
{
"allocatedIds": [
"default",
],
"meta": {
"id": undefined,
"tags": [
"a",
],
"title": "MyStory",
},
"stories": {},
}
`);
});
test('Duplicate Id', () => {
expect(
extractStories(`
Expand Down
106 changes: 73 additions & 33 deletions src/parser/extract-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,37 @@ interface StoriesDef {
allocatedIds: string[];
}

function lookupAttribute(name: string, attributes: any[]) {
return attributes.find((att: any) =>
(att.type === 'Attribute' && att.name === name) ||
(att.type === 'Property' && att.key.name === name));
}

function getStaticAttribute(name: string, node: any): string | undefined {
// extract the attribute
const attribute = node.attributes.find(
(att: any) => att.type === 'Attribute' && att.name === name
);
const attribute = lookupAttribute(name, node);

if (!attribute) {
return undefined;
}

const { value } = attribute;
// expect the attribute to be static, ie only one Text node
// expect the attribute to be static, ie only one Text node or Literal
if (value?.type === 'Literal') {
return value.value;
}

if (value && value.length === 1 && value[0].type === 'Text') {
return value[0].data;
}

throw new Error(`Attribute ${name} is not static`);
}

function getStaticBooleanAttribute(name: string, node: any): boolean | undefined {
function getStaticBooleanAttribute(name: string, attributes: any[]): boolean | undefined {
// extract the attribute
const attribute = node.attributes.find(
(att: any) => att.type === 'Attribute' && att.name === name
);
const attribute = lookupAttribute(name, attributes);


if (!attribute) {
return undefined;
Expand All @@ -62,27 +69,35 @@ function getStaticBooleanAttribute(name: string, node: any): boolean | undefined
throw new Error(`Attribute ${name} is not a static boolean`);
}

function getMetaTags(node: any): string[] {
function getMetaTags(attributes: any[]): string[] {

const finalTags = getStaticBooleanAttribute('autodocs', attributes) ? ["autodocs"] : [];

const finalTags = getStaticBooleanAttribute('autodocs', node) ? ["autodocs"] : [];
const tags = lookupAttribute('tags', attributes);

const tags = node.attributes.find((att: any) => att.type === 'Attribute' && att.name === 'tags');
if (tags) {
let valid = false;

const { value } = tags;
if (value && value.length === 1 ) {
const { type, expression, data } = value[0];
if (type === 'Text') {
// tags="autodocs"
finalTags.push(data);
valid = true;
} else if (type === 'MustacheTag' && expression.type === 'ArrayExpression') {
// tags={["autodocs"]}
const { elements } = expression;
elements.forEach((e : any) => finalTags.push(e.value));
valid = true;
}
let { value } = tags;
if (value && value.length === 1) {
value = value[0];
}

const { type, expression, data } = value;
if (type === 'Text') {
// tags="autodocs"
finalTags.push(data);
valid = true;
} else if (type === 'ArrayExpression') {
// tags={["autodocs"]} in object
const { elements } = value;
elements.forEach((e : any) => finalTags.push(e.value));
valid = true;
} else if (type === 'MustacheTag' && expression.type === 'ArrayExpression') {
// tags={["autodocs"]} in template
const { elements } = expression;
elements.forEach((e : any) => finalTags.push(e.value));
valid = true;
}

if (!valid) {
Expand All @@ -93,6 +108,14 @@ function getMetaTags(node: any): string[] {
return finalTags;
}

function fillMetaFromAttributes(meta: MetaDef, attributes: any[]) {
meta.title = getStaticAttribute('title', attributes);
meta.id = getStaticAttribute('id', attributes);
const tags = getMetaTags(attributes);
if (tags.length > 0) {
meta.tags = tags;
}
}
/**
* Parse a Svelte component and extract stories.
* @param component Component Source
Expand Down Expand Up @@ -142,6 +165,28 @@ export function extractStories(component: string): StoriesDef {

const stories: Record<string, StoryDef> = {};
const meta: MetaDef = {};
if (ast.module) {
svelte.walk(<Node>ast.module.content, {
enter(node: any) {
if (node.type === 'ExportNamedDeclaration' &&
node.declaration?.type === 'VariableDeclaration' &&
node.declaration?.declarations.length === 1 &&
node.declaration?.declarations[0]?.id?.name === 'meta') {

if (node.declaration?.kind !== 'const') {
throw new Error('meta should be exported as const');
}

const init = node.declaration?.declarations[0]?.init;
if (init?.type !== 'ObjectExpression') {
throw new Error('meta should export on object');
}

fillMetaFromAttributes(meta, init.properties);
}
}
});
}
svelte.walk(<Node>ast.html, {
enter(node: any) {
if (
Expand All @@ -153,7 +198,7 @@ export function extractStories(component: string): StoriesDef {
const isTemplate = node.name === 'Template';

// extract the 'name' attribute
let name = getStaticAttribute('name', node);
let name = getStaticAttribute('name', node.attributes);

// templates has a default name
if (!name && isTemplate) {
Expand All @@ -162,7 +207,7 @@ export function extractStories(component: string): StoriesDef {

const id = extractId(
{
id: getStaticAttribute('id', node),
id: getStaticAttribute('id', node.attributes),
name,
},
isTemplate ? undefined : allocatedIds
Expand All @@ -189,16 +234,11 @@ export function extractStories(component: string): StoriesDef {
} else if (node.type === 'InlineComponent' && node.name === localNames.Meta) {
this.skip();

meta.title = getStaticAttribute('title', node);
meta.id = getStaticAttribute('id', node);
const tags = getMetaTags(node);
if (tags.length > 0) {
meta.tags = tags;
}
fillMetaFromAttributes(meta, node.attributes);
}
},
});

return {
meta,
stories,
Expand Down
7 changes: 4 additions & 3 deletions src/parser/svelte-stories-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ function transformSvelteStories(code: string) {
.map(([id]) => `export const ${id} = __storiesMetaData.stories[${JSON.stringify(id)}]`)
.join('\n');

const codeWithoutDefaultExport = code.replace('export default ', '//export default');

const metaExported = code.includes('export { meta }');
const codeWithoutDefaultExport = code.replace('export default ', '//export default').replace('export { meta };', '// export { meta };');

return `${codeWithoutDefaultExport}
const { default: parser } = require('${parser}');
const __storiesMetaData = parser(${componentName}, ${JSON.stringify(storiesDef)});
const __storiesMetaData = parser(${componentName}, ${JSON.stringify(storiesDef)}${metaExported ? ', meta' : ''});
export default __storiesMetaData.meta;
${storyDef};
` as string;
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/vite-svelte-csf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ export default function csfPlugin(svelteOptions) {
.filter(([, def]) => !def.template)
.map(([storyId]) => storyId);

const metaExported = code.includes('export { meta }');
s.replace('export { meta };', '// export { meta };');
const output = [
'',
`import parser from '${parser}';`,
`const __storiesMetaData = parser(${component}, ${JSON.stringify(all)});`,
`const __storiesMetaData = parser(${component}, ${JSON.stringify(all)}${metaExported ? ', meta' : ''});`,
'export default __storiesMetaData.meta;',
`export const __namedExportsOrder = ${JSON.stringify(namedExportsOrder)};`,
storyDef,
Expand Down
35 changes: 35 additions & 0 deletions stories/metaexport.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script context='module'>
import Button from './Button.svelte';
export const meta = {
title: 'MetaExport',
component: Button,
tags: ['autodocs']
}
</script>

<script>
import { Story, Template } from '../src/index';
let count = 0;
function handleClick() {
count += 1;
}
</script>

<Template let:args>
<Button {...args} on:click={handleClick}>
You clicked: {count}
</Button>
</Template>

<Story name="Default"/>

<Story name="Rounded" args={{rounded: true}}/>

<Story name="Square" source args={{rounded: false}}/>

<!-- Dynamic snippet should be disabled for this story -->
<Story name="Button No Args">
<Button>Label</Button>
</Story>

0 comments on commit 74c982c

Please sign in to comment.