Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svelte: Native Svelte CSF #13772

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addons/docs/src/frameworks/svelte/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Configuration } from 'webpack';
export function webpackFinal(webpackConfig: Configuration, options: any = {}) {
webpackConfig.module.rules.push({
test: /\.svelte$/,
exclude: /\.stories\.svelte$/,
loader: path.resolve(`${__dirname}/svelte-docgen-loader`),
enforce: 'post',
});
Expand Down
4 changes: 4 additions & 0 deletions addons/docs/src/frameworks/svelte/sourceDecorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export function generateSvelteSource(
argTypes: ArgTypes,
slotProperty: string
): string {
if (!component) {
return null;
}

const name = getComponentName(component);

if (!name) {
Expand Down
23 changes: 23 additions & 0 deletions app/svelte/jest-transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const svelte = require('svelte/compiler');

const parser = require.resolve('./src/client/parse-stories').replace(/[/\\]/g, '/');

function process(src, filename) {
const result = svelte.compile(src, {
format: 'cjs',
filename,
});

const code = result.js ? result.js.code : result.code;

const z = {
code: `${code}
const { default: parser } = require('${parser}');
module.exports = parser(module.exports, {});
Object.defineProperty(exports, "__esModule", { value: true });`,
map: result.js ? result.js.map : result.map,
};
return z;
}

exports.process = process;
2 changes: 2 additions & 0 deletions app/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
},
"dependencies": {
"@storybook/addons": "6.2.0-alpha.25",
"@storybook/client-api": "6.2.0-alpha.25",
"@storybook/client-logger": "6.2.0-alpha.25",
"@storybook/core": "6.2.0-alpha.25",
"core-js": "^3.8.2",
"global": "^4.4.0",
Expand Down
6 changes: 6 additions & 0 deletions app/svelte/src/client/components/Meta.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
import { useContext } from './context';

useContext().meta = $$props;
</script>

11 changes: 11 additions & 0 deletions app/svelte/src/client/components/RegisterContext.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import { createRegistrationContext } from './context';

export let Stories;
export let repositories;

createRegistrationContext(repositories);

</script>

<svelte:component this={Stories} />
13 changes: 13 additions & 0 deletions app/svelte/src/client/components/RenderContext.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
/**
* @component
* @wrapper
*/
import { createRenderContext } from './context';

export let Stories;

createRenderContext($$props);
</script>

<svelte:component this={Stories}/>
24 changes: 24 additions & 0 deletions app/svelte/src/client/components/Story.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script>
import { useContext } from './context';

const context = useContext();

export let name;
export let template;

if (!name) {
throw new Error('Missing Story name');
}

context.register({
name,
...$$restProps,
template: template != null ? template : !$$slots.default ? 'default' : null,
});

$: render = context.render && !context.templateName && context.storyName == name;
</script>

{#if render}
<slot args={context.args} {...context.args}/>
{/if}
15 changes: 15 additions & 0 deletions app/svelte/src/client/components/Template.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
import { useContext } from './context';

const context = useContext();

export let name = 'default';

context.register({name, isTemplate: true});

$: render = context.render && context.templateName === name;
</script>

{#if render}
<slot args={context.args} {...context.args}/>
{/if}
21 changes: 21 additions & 0 deletions app/svelte/src/client/components/__tests__/TestStories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script>
import Meta from '../Meta.svelte';
import Story from '../Story.svelte';
import Template from '../Template.svelte';
</script>

<Meta title="Test"/>

<Template name="tpl1">
<div>tpl1</div>
</Template>

<Template name="tpl2">
<div>tpl2</div>
</Template>

<Story name="Story1" template="tpl1" args={{tpl1:true}}/>
<Story name="Story2" template="tpl2" source args={{tpl1:true}}/>
<Story name="Story3" source="xyz">
<div>story3</div>
</Story>
34 changes: 34 additions & 0 deletions app/svelte/src/client/components/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getContext, hasContext, setContext } from 'svelte';

const CONTEXT_KEY = 'storybook-registration-context';

export function createRenderContext(props: any = {}) {
setContext(CONTEXT_KEY, {
render: true,
register: () => {},
meta: {},
args: {},
...props,
});
}

export function createRegistrationContext(repositories: any) {
setContext(CONTEXT_KEY, {
render: false,
register: (story: any) => {
repositories.stories.push(story);
},
set meta(value: any) {
// eslint-disable-next-line no-param-reassign
repositories.meta = value;
},
args: {},
});
}

export function useContext() {
if (!hasContext(CONTEXT_KEY)) {
createRenderContext();
}
return getContext(CONTEXT_KEY);
}
5 changes: 5 additions & 0 deletions app/svelte/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export {
raw,
} from './preview';

export { default as Meta } from './components/Meta.svelte';
export { default as Story } from './components/Story.svelte';
export { default as Template } from './components/Template.svelte';
export { useContext } from './components/context';

if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
131 changes: 131 additions & 0 deletions app/svelte/src/client/parse-stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* eslint-env browser */
import { logger } from '@storybook/client-logger';
import { combineParameters } from '@storybook/client-api';

import RegisterContext from './components/RegisterContext.svelte';
import RenderContext from './components/RenderContext.svelte';

/* Called from a webpack loader and a jest transformation.
*
* It mounts a Stories component in a context which disables
* the rendering of every <Story/> and <Template/> but instead
* collects names and properties.
*
* For every discovered <Story/>, it creates a storyFn which
* instantiate the main Stories component: Every Story but
* the one selected is disabled.
*/

const createFragment = document.createDocumentFragment
? () => document.createDocumentFragment()
: () => document.createElement('div');

export default (module, sources) => {
const { default: Stories } = module;

const repositories = {
meta: null,
stories: [],
};

// extract all stories
try {
const context = new RegisterContext({
target: createFragment(),
props: {
Stories,
repositories,
},
});
context.$destroy();
} catch (e) {
logger.error(`Error extracting stories ${e.toString()}`);
}

const { meta } = repositories;
if (!meta) {
logger.error('Missing <Meta/> tag');
return {};
}

const { component: globalComponent } = meta;

// collect templates id
const templatesId = repositories.stories
.filter((story) => story.isTemplate)
.map((story) => story.name);

// check for duplicate templates
const duplicateTemplatesId = templatesId.filter(
(item, index) => templatesId.indexOf(item) !== index
);

if (duplicateTemplatesId.length > 0) {
logger.warn(`Found duplicates templates id :${duplicateTemplatesId}`);
}

const found = repositories.stories
.filter((story) => !story.isTemplate)
.map((story) => {
const { name, template, component, source = false, ...props } = story;

const unknowTemplate = template != null && templatesId.indexOf(template) < 0;

const storyFn = (args) => {
if (unknowTemplate) {
throw new Error(`Story ${name} is referencing an unknown template ${template}`);
}

return {
Component: RenderContext,
props: {
Stories,
storyName: name,
templateName: template,
args,
sourceComponent: component || globalComponent,
},
};
};

storyFn.storyName = name;
Object.entries(props).forEach(([k, v]) => {
storyFn[k] = v;
});

// inject story sources
const storySource = sources[template ? `tpl:${template}` : name];
const rawSource = storySource ? storySource.source : null;
if (rawSource) {
storyFn.parameters = combineParameters(storyFn.parameters || {}, {
storySource: {
source: rawSource,
},
});
}

// inject source snippet
const hasArgs = storySource ? storySource.hasArgs : true;

let snippet;

if (source === true || (source === false && !hasArgs)) {
snippet = rawSource;
} else if (typeof source === 'string') {
snippet = source;
}

if (snippet) {
storyFn.parameters = combineParameters(storyFn.parameters || {}, {
docs: { source: { code: snippet } },
});
}

return storyFn;
});

return {
default: meta,
...found,
};
};
28 changes: 28 additions & 0 deletions app/svelte/src/client/parse-stories.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import parseStories from './parse-stories';
import TestStories from './components/__tests__/TestStories.svelte';

describe('parse-stories', () => {
test('Extract Stories', () => {
const stories = parseStories({ default: TestStories }, { 'tpl:tpl2': 'tpl2src' });
expect(stories.default).toMatchInlineSnapshot(`
Object {
"title": "Test",
}
`);

expect(stories['0'].storyName).toBe('Story1');
expect(stories['0'].parameters).toMatchInlineSnapshot(`undefined`);
expect(stories['1'].storyName).toBe('Story2');
expect(stories['1'].parameters).toMatchInlineSnapshot(`undefined`);
expect(stories['2'].storyName).toBe('Story3');
expect(stories['2'].parameters).toMatchInlineSnapshot(`
Object {
"docs": Object {
"source": Object {
"code": "xyz",
},
},
}
`);
});
});
Loading