diff --git a/addons/actions/src/preview/decorateAction.ts b/addons/actions/src/preview/decorateAction.ts index df7764e51a2e..c60afc6d73c1 100644 --- a/addons/actions/src/preview/decorateAction.ts +++ b/addons/actions/src/preview/decorateAction.ts @@ -5,7 +5,7 @@ import { ActionOptions, DecoratorFunction, HandlerFunction } from '../models'; const applyDecorators = (decorators: DecoratorFunction[], actionCallback: HandlerFunction) => { return (..._args: any[]) => { - const decorated = decorators.reduce((args, fn) => fn(args), _args); + const decorated = decorators.reduce((args, storyFn) => storyFn(args), _args); actionCallback(...decorated); }; }; diff --git a/addons/actions/src/preview/withActions.ts b/addons/actions/src/preview/withActions.ts index efd121936694..17ccf48ae04f 100644 --- a/addons/actions/src/preview/withActions.ts +++ b/addons/actions/src/preview/withActions.ts @@ -56,11 +56,11 @@ const actionsSubscription = (...args: any[]) => { return lastSubscription; }; -export const createDecorator = (actionsFn: any) => (...args: any[]) => (story: () => any) => { +export const createDecorator = (actionsFn: any) => (...args: any[]) => (storyFn: () => any) => { if (root != null) { addons.getChannel().emit(Events.REGISTER_SUBSCRIPTION, actionsSubscription(actionsFn, ...args)); } - return story(); + return storyFn(); }; export const withActions = createDecorator(actions); diff --git a/addons/centered/src/html.js b/addons/centered/src/html.js index 8060fabcee0a..88e3e8ab4b70 100644 --- a/addons/centered/src/html.js +++ b/addons/centered/src/html.js @@ -31,15 +31,15 @@ export default function(storyFn) { const wrapper = getWrapperDiv(); wrapper.appendChild(inner); - const component = storyFn(); + const element = storyFn(); - if (typeof component === 'string') { - inner.innerHTML = component; - } else if (component instanceof Node) { + if (typeof element === 'string') { + inner.innerHTML = element; + } else if (element instanceof Node) { inner.innerHTML = ''; - inner.appendChild(component); + inner.appendChild(element); } else { - return component; + return element; } return wrapper; diff --git a/addons/events/src/index.js b/addons/events/src/index.js index 5b12b24b3f3d..31eb1960e36f 100644 --- a/addons/events/src/index.js +++ b/addons/events/src/index.js @@ -39,9 +39,9 @@ export default options => { if (options.children) { return WithEvents(options); } - return story => { + return storyFn => { addEvents(options); - return story(); + return storyFn(); }; }; diff --git a/addons/jest/src/index.js b/addons/jest/src/index.js index 26db47c59601..8d41d435bab1 100644 --- a/addons/jest/src/index.js +++ b/addons/jest/src/index.js @@ -38,14 +38,14 @@ export const withTests = userOptions => { return (...args) => { if (typeof args[0] === 'string') { - return deprecate((story, { kind }) => { - emitAddTests({ kind, story, testFiles: args, options }); + return deprecate((storyFn, { kind }) => { + emitAddTests({ kind, story: storyFn, testFiles: args, options }); - return story(); + return storyFn(); }, 'Passing component filenames to the `@storybook/addon-jest` via `withTests` is deprecated. Instead, use the `jest` story parameter'); } - const [story, { kind, parameters = {} }] = args; + const [storyFn, { kind, parameters = {} }] = args; let { jest: testFiles } = parameters; if (typeof testFiles === 'string') { @@ -53,10 +53,10 @@ export const withTests = userOptions => { } if (testFiles && !testFiles.disable) { - emitAddTests({ kind, story, testFiles, options }); + emitAddTests({ kind, story: storyFn, testFiles, options }); } - return story(); + return storyFn(); }; }; diff --git a/addons/links/src/preview.js b/addons/links/src/preview.js index 938380e90ba7..816426e449d5 100644 --- a/addons/links/src/preview.js +++ b/addons/links/src/preview.js @@ -55,8 +55,8 @@ const off = () => { } }; -export const withLinks = story => { +export const withLinks = storyFn => { on(); addons.getChannel().once(STORY_CHANGED, off); - return story(); + return storyFn(); }; diff --git a/addons/storyshots/storyshots-core/src/api/index.js b/addons/storyshots/storyshots-core/src/api/index.js index dcb833af3b3c..a764f0a26b25 100644 --- a/addons/storyshots/storyshots-core/src/api/index.js +++ b/addons/storyshots/storyshots-core/src/api/index.js @@ -48,7 +48,7 @@ function testStorySnapshots(options = {}) { .filter(({ name }) => (storyNameRegex ? name.match(storyNameRegex) : true)) .filter(({ kind }) => (storyKindRegex ? kind.match(storyKindRegex) : true)) .reduce((acc, item) => { - const { kind, story: render, parameters } = item; + const { kind, storyFn: render, parameters } = item; const existing = acc.find(i => i.kind === kind); const { fileName } = item.parameters; diff --git a/addons/storyshots/storyshots-core/src/frameworks/riot/renderTree.js b/addons/storyshots/storyshots-core/src/frameworks/riot/renderTree.js index 77f81e12fa32..c95ccf8ff7f8 100644 --- a/addons/storyshots/storyshots-core/src/frameworks/riot/renderTree.js +++ b/addons/storyshots/storyshots-core/src/frameworks/riot/renderTree.js @@ -13,9 +13,9 @@ function bootstrapADocumentAndReturnANode() { function makeSureThatResultIsRenderedSomehow({ context, result, rootElement }) { if (!rootElement.firstChild) { riotForStorybook.render({ - story: () => result, + storyFn: () => result, selectedKind: context.kind, - selectedStory: context.story, + selectedStory: context.name, }); } } diff --git a/addons/storysource/src/preview.js b/addons/storysource/src/preview.js index 7b516ff53689..1aa601caa954 100644 --- a/addons/storysource/src/preview.js +++ b/addons/storysource/src/preview.js @@ -17,8 +17,8 @@ function setStorySource(context, source, locationsMap) { } export function withStorySource(source, locationsMap = {}) { - return (story, context) => { + return (storyFn, context) => { setStorySource(context, source, locationsMap); - return story(); + return storyFn(); }; } diff --git a/app/angular/src/client/preview/angular/helpers.ts b/app/angular/src/client/preview/angular/helpers.ts index 7d5df639c1c3..f5e218d75b33 100644 --- a/app/angular/src/client/preview/angular/helpers.ts +++ b/app/angular/src/client/preview/angular/helpers.ts @@ -4,7 +4,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './components/app.component'; import { STORY } from './app.token'; -import { NgModuleMetadata, IGetStory, NgStory } from './types'; +import { NgModuleMetadata, IStoryFn, NgStory } from './types'; let platform: any = null; let promises: Array>> = []; @@ -40,8 +40,8 @@ const createComponentFromTemplate = (template: string, styles: string[]) => { })(componentClass); }; -const initModule = (currentStory: IGetStory) => { - const storyObj = currentStory(); +const initModule = (storyFn: IStoryFn) => { + const storyObj = storyFn(); const { component, template, props, styles, moduleMetadata = {} } = storyObj; let AnnotatedComponent = template ? createComponentFromTemplate(template, styles) : component; @@ -80,6 +80,6 @@ const draw = (newModule: DynamicComponentType): void => { } }; -export const renderNgApp = (story: IGetStory) => { - draw(initModule(story)); +export const renderNgApp = (storyFn: IStoryFn) => { + draw(initModule(storyFn)); }; diff --git a/app/angular/src/client/preview/angular/types.ts b/app/angular/src/client/preview/angular/types.ts index 317aa02a9bdb..0633eabea0ea 100644 --- a/app/angular/src/client/preview/angular/types.ts +++ b/app/angular/src/client/preview/angular/types.ts @@ -19,4 +19,4 @@ export interface NgStory { styles?: string[]; } -export type IGetStory = () => NgStory; +export type IStoryFn = () => NgStory; diff --git a/app/angular/src/client/preview/render.js b/app/angular/src/client/preview/render.js index 9b0b027d0c38..c191b41d0789 100644 --- a/app/angular/src/client/preview/render.js +++ b/app/angular/src/client/preview/render.js @@ -1,6 +1,6 @@ import { renderNgApp } from './angular/helpers'; -export default function render({ story, showMain }) { +export default function render({ storyFn, showMain }) { showMain(); - renderNgApp(story); + renderNgApp(storyFn); } diff --git a/app/ember/src/client/preview/render.js b/app/ember/src/client/preview/render.js index f9c1ba6e0a25..ec81aaea2e34 100644 --- a/app/ember/src/client/preview/render.js +++ b/app/ember/src/client/preview/render.js @@ -50,8 +50,15 @@ function render(options, el) { }); } -export default function renderMain({ story, selectedKind, selectedStory, showMain, showError }) { - const element = story(); +export default function renderMain({ + storyFn, + selectedKind, + selectedStory, + showMain, + showError, + // forceRender, +}) { + const element = storyFn(); if (!element) { showError({ diff --git a/app/html/src/client/preview/render.js b/app/html/src/client/preview/render.js index db2632aa91e2..a3898b72346f 100644 --- a/app/html/src/client/preview/render.js +++ b/app/html/src/client/preview/render.js @@ -4,25 +4,25 @@ import { stripIndents } from 'common-tags'; const rootElement = document.getElementById('root'); export default function renderMain({ - story, + storyFn, selectedKind, selectedStory, showMain, showError, forceRender, }) { - const component = story(); + const element = storyFn(); showMain(); - if (typeof component === 'string') { - rootElement.innerHTML = component; - } else if (component instanceof Node) { + if (typeof element === 'string') { + rootElement.innerHTML = element; + } else if (element instanceof Node) { if (forceRender === true) { return; } rootElement.innerHTML = ''; - rootElement.appendChild(component); + rootElement.appendChild(element); } else { showError({ title: `Expecting an HTML snippet or DOM node from the story: "${selectedStory}" of "${selectedKind}".`, diff --git a/app/marko/src/client/preview/render.js b/app/marko/src/client/preview/render.js index 50b892aaaf70..9b63a4317439 100644 --- a/app/marko/src/client/preview/render.js +++ b/app/marko/src/client/preview/render.js @@ -4,8 +4,15 @@ import { stripIndents } from 'common-tags'; const rootEl = document.getElementById('root'); let currLoadedComponent = null; // currently loaded marko widget! -export default function renderMain({ story, selectedKind, selectedStory, showMain, showError }) { - const element = story(); +export default function renderMain({ + storyFn, + selectedKind, + selectedStory, + showMain, + showError, + // forceRender, +}) { + const element = storyFn(); // We need to unmount the existing set of components in the DOM node. if (currLoadedComponent) { diff --git a/app/mithril/src/client/preview/render.js b/app/mithril/src/client/preview/render.js index 39b14981cda4..1760e37ab7f4 100644 --- a/app/mithril/src/client/preview/render.js +++ b/app/mithril/src/client/preview/render.js @@ -6,8 +6,15 @@ import { stripIndents } from 'common-tags'; const rootEl = document.getElementById('root'); -export default function renderMain({ story, selectedKind, selectedStory, showMain, showError }) { - const element = story(); +export default function renderMain({ + storyFn, + selectedKind, + selectedStory, + showMain, + showError, + // forceRender, +}) { + const element = storyFn(); if (!element) { const error = { diff --git a/app/polymer/src/client/preview/render.js b/app/polymer/src/client/preview/render.js index 6fbbe908c21f..c290eb4dc374 100644 --- a/app/polymer/src/client/preview/render.js +++ b/app/polymer/src/client/preview/render.js @@ -5,16 +5,16 @@ import { html, render, TemplateResult } from 'lit-html'; const rootElement = document.getElementById('root'); export default function renderMain({ - story, + storyFn, selectedKind, selectedStory, showMain, showError, forceRender, }) { - const component = story(); + const element = storyFn(); - if (!component) { + if (!element) { showError({ title: `Expecting a Polymer component from the story: "${selectedStory}" of "${selectedKind}".`, description: stripIndents` @@ -26,18 +26,18 @@ export default function renderMain({ } showMain(); - if (typeof component === 'string') { - rootElement.innerHTML = component; - } else if (component instanceof TemplateResult) { + if (typeof element === 'string') { + rootElement.innerHTML = element; + } else if (element instanceof TemplateResult) { // `render` stores the TemplateInstance in the Node and tries to update based on that. // Since we reuse `rootElement` for all stories, remove the stored instance first. // But forceRender means that it's the same story, so we want too keep the state in that case. if (!forceRender) { render(html``, rootElement); } - render(component, rootElement); + render(element, rootElement); } else { rootElement.innerHTML = ''; - rootElement.appendChild(component); + rootElement.appendChild(element); } } diff --git a/app/preact/src/client/preview/render.js b/app/preact/src/client/preview/render.js index 964c606470cf..edb9cf188f98 100644 --- a/app/preact/src/client/preview/render.js +++ b/app/preact/src/client/preview/render.js @@ -6,8 +6,15 @@ import { stripIndents } from 'common-tags'; let renderedStory; const rootElement = document ? document.getElementById('root') : null; -export default function renderMain({ story, selectedKind, selectedStory, showMain, showError }) { - const element = story(); +export default function renderMain({ + storyFn, + selectedKind, + selectedStory, + showMain, + showError, + // forceRender, +}) { + const element = storyFn(); if (!element) { showError({ diff --git a/app/react/src/client/preview/render.js b/app/react/src/client/preview/render.js index d4bd2627c16d..8ba7d5cc11ab 100644 --- a/app/react/src/client/preview/render.js +++ b/app/react/src/client/preview/render.js @@ -14,14 +14,14 @@ function render(node, el) { } export default function renderMain({ - story, + storyFn, selectedKind, selectedStory, showMain, showError, forceRender, }) { - const element = story(); + const element = storyFn(); if (!element) { showError({ diff --git a/app/riot/src/client/preview/render.js b/app/riot/src/client/preview/render.js index e332210d03cf..1e8651d3f79b 100644 --- a/app/riot/src/client/preview/render.js +++ b/app/riot/src/client/preview/render.js @@ -4,7 +4,7 @@ import { unregister } from 'riot'; import { render as renderRiot } from './rendering'; export default function renderMain({ - story, + storyFn, selectedKind, selectedStory, showMain = () => {}, @@ -15,9 +15,9 @@ export default function renderMain({ const rootElement = document.getElementById('root'); rootElement.innerHTML = ''; rootElement.dataset.is = 'root'; - const component = story(); - const rendered = renderRiot(component); - if (!rendered) + const element = storyFn(); + const rendered = renderRiot(element); + if (!rendered) { showError({ title: `Expecting a riot snippet or a riot component from the story: "${selectedStory}" of "${selectedKind}".`, description: stripIndents` @@ -25,5 +25,6 @@ export default function renderMain({ Use "() => " or when defining the story. `, }); + } return rendered; } diff --git a/app/svelte/src/client/preview/render.js b/app/svelte/src/client/preview/render.js index a5e1f85d2598..4c80f788f1fb 100644 --- a/app/svelte/src/client/preview/render.js +++ b/app/svelte/src/client/preview/render.js @@ -42,7 +42,7 @@ function mountView({ Component, target, data, on, Wrapper, WrapperData }) { } export default function render({ - story, + storyFn, selectedKind, selectedStory, showMain, @@ -58,7 +58,7 @@ export default function render({ on, Wrapper, WrapperData, - } = story(); + } = storyFn(); cleanUpPreviousStory(); diff --git a/app/vue/src/client/preview/render.js b/app/vue/src/client/preview/render.js index e3227b3e71f9..93072b8071ba 100644 --- a/app/vue/src/client/preview/render.js +++ b/app/vue/src/client/preview/render.js @@ -18,7 +18,7 @@ const root = new Vue({ }); export default function render({ - story, + storyFn, selectedKind, selectedStory, showMain, @@ -28,9 +28,9 @@ export default function render({ }) { Vue.config.errorHandler = showException; - const component = story(); + const element = storyFn(); - if (!component) { + if (!element) { showError({ title: `Expecting a Vue component from the story: "${selectedStory}" of "${selectedKind}".`, description: stripIndents` @@ -45,10 +45,10 @@ export default function render({ // at component creation || refresh by HMR if (!root[COMPONENT] || !forceRender) { - root[COMPONENT] = component; + root[COMPONENT] = element; } - root[VALUES] = component.options[VALUES]; + root[VALUES] = element.options[VALUES]; if (!root.$el) { root.$mount('#root'); diff --git a/docs/.storybook/config.js b/docs/.storybook/config.js index ac173268f49d..85850bef9c40 100644 --- a/docs/.storybook/config.js +++ b/docs/.storybook/config.js @@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router'; import 'bootstrap/dist/css/bootstrap.css'; import '../src/css/main.css'; -addDecorator(story => {story()}); +addDecorator(storyFn => {storyFn()}); function loadStories() { require('../src/stories'); diff --git a/docs/src/pages/basics/writing-stories/index.md b/docs/src/pages/basics/writing-stories/index.md index 20d87e9ad06b..1a6167ad0b95 100644 --- a/docs/src/pages/basics/writing-stories/index.md +++ b/docs/src/pages/basics/writing-stories/index.md @@ -79,7 +79,7 @@ import { storiesOf } from '@storybook/react'; import MyComponent from '../my_component'; storiesOf('MyComponent', module) - .addDecorator(story =>
{story()}
) + .addDecorator(storyFn =>
{storyFn()}
) .add('without props', () => ) .add('with some props', () => ); ``` @@ -92,7 +92,7 @@ It is possible to apply a decorator **globally** to all the stories. Here is an import React from 'react'; import { configure, addDecorator } from '@storybook/react'; -addDecorator(story =>
{story()}
); +addDecorator(storyFn =>
{storyFn()}
); configure(function() { // ... diff --git a/examples/html-kitchen-sink/stories/addon-events.stories.js b/examples/html-kitchen-sink/stories/addon-events.stories.js index 954f7eeea87a..5da5b3b9c38a 100644 --- a/examples/html-kitchen-sink/stories/addon-events.stories.js +++ b/examples/html-kitchen-sink/stories/addon-events.stories.js @@ -85,9 +85,9 @@ storiesOf('Addons|Events', module) ], }) ) - .addDecorator(story => { + .addDecorator(storyFn => { addons.getChannel().emit(CoreEvents.REGISTER_SUBSCRIPTION, subscription); - return story(); + return storyFn(); }) .add( 'Logger', diff --git a/examples/official-storybook/config.js b/examples/official-storybook/config.js index be51e97003bd..cefbf2d199d3 100644 --- a/examples/official-storybook/config.js +++ b/examples/official-storybook/config.js @@ -33,10 +33,10 @@ addDecorator(withCssResources); addDecorator(withA11Y); addDecorator(withNotes); -addDecorator(fn => ( +addDecorator(storyFn => ( - {fn()} + {storyFn()} )); diff --git a/examples/official-storybook/stories/addon-events.stories.js b/examples/official-storybook/stories/addon-events.stories.js index afa8dde8ad72..8c17d0a917a5 100644 --- a/examples/official-storybook/stories/addon-events.stories.js +++ b/examples/official-storybook/stories/addon-events.stories.js @@ -83,9 +83,9 @@ storiesOf('Addons|Events.deprecated', module) selectedPanel: 'storybook/events/panel', }, }) - .addDecorator(story => ( + .addDecorator(storyFn => ( - {story()} + {storyFn()} )) .add('Logger', () => ); diff --git a/examples/official-storybook/stories/addon-links.stories.js b/examples/official-storybook/stories/addon-links.stories.js index b2d00fb73792..c5f6a148ba6d 100644 --- a/examples/official-storybook/stories/addon-links.stories.js +++ b/examples/official-storybook/stories/addon-links.stories.js @@ -49,10 +49,10 @@ storiesOf('Addons|Links.Href', module).add( ); storiesOf('Addons|Links.Scroll position', module) - .addDecorator(story => ( + .addDecorator(storyFn => (
Scroll down to see the link
- {story()} + {storyFn()}
)) .add('First', () => Go to Second) diff --git a/examples/polymer-cli/src/stories/custom-decorators.stories.js b/examples/polymer-cli/src/stories/custom-decorators.stories.js index c2ef4cdf3ddb..158b8be733cc 100644 --- a/examples/polymer-cli/src/stories/custom-decorators.stories.js +++ b/examples/polymer-cli/src/stories/custom-decorators.stories.js @@ -2,8 +2,8 @@ import { storiesOf } from '@storybook/polymer'; import { document } from 'global'; storiesOf('Custom|Decorator', module) - .addDecorator(story => { - const el = story(); + .addDecorator(storyFn => { + const el = storyFn(); el.setAttribute('title', `${el.getAttribute('title')} - decorated`); return el; }) diff --git a/examples/vue-kitchen-sink/src/stories/custom-decorators.stories.js b/examples/vue-kitchen-sink/src/stories/custom-decorators.stories.js index 3ff256088776..3c3a022d3cdd 100644 --- a/examples/vue-kitchen-sink/src/stories/custom-decorators.stories.js +++ b/examples/vue-kitchen-sink/src/stories/custom-decorators.stories.js @@ -3,9 +3,9 @@ import { storiesOf } from '@storybook/vue'; import MyButton from './Button.vue'; storiesOf('Custom|Decorator for Vue', module) - .addDecorator(story => { - // Decorated with story function - const WrapButton = story(); + .addDecorator(storyFn => { + // Decorated with story-function + const WrapButton = storyFn(); return { components: { WrapButton }, template: '
', diff --git a/lib/client-api/src/client_api.js b/lib/client-api/src/client_api.js index cb13752d4830..238a2ff1e719 100644 --- a/lib/client-api/src/client_api.js +++ b/lib/client-api/src/client_api.js @@ -4,7 +4,6 @@ import isPlainObject from 'is-plain-object'; import { logger } from '@storybook/client-logger'; import addons from '@storybook/addons'; import Events from '@storybook/core-events'; -import memoize from 'memoizerific'; import mergeWith from 'lodash.mergewith'; import isEqual from 'lodash.isequal'; @@ -31,7 +30,7 @@ const merge = (a, b) => return undefined; }); -export const defaultDecorateStory = (getStory, decorators) => +export const defaultDecorateStory = (storyFn, decorators) => decorators.reduce( (decorated, decorator) => (context = {}) => decorator( @@ -47,7 +46,7 @@ export const defaultDecorateStory = (getStory, decorators) => ), context ), - getStory + storyFn ); const metaSubscription = () => { @@ -57,7 +56,9 @@ const metaSubscription = () => { }; const withSubscriptionTracking = storyFn => { - if (!addons.hasChannel()) return storyFn(); + if (!addons.hasChannel()) { + return storyFn(); + } subscriptionsStore.markAllAsUnused(); subscriptionsStore.register(metaSubscription); const result = storyFn(); @@ -145,7 +146,7 @@ export default class ClientApi { }; }); - api.add = (storyName, getStory, parameters) => { + api.add = (storyName, storyFn, parameters) => { const { _globalParameters, _globalDecorators } = this; const id = toId(kind, storyName); @@ -159,11 +160,6 @@ export default class ClientApi { }); } - // Wrap the getStory function with each decorator. The first - // decorator will wrap the story function. The second will - // wrap the first decorator and so on. - const decorators = [...localDecorators, ..._globalDecorators, withSubscriptionTracking]; - const fileName = m ? m.id : null; const { hierarchyRootSeparator, hierarchySeparator } = this.getSeparators(); @@ -196,15 +192,19 @@ export default class ClientApi { { fileName } ); - this._storyStore.addStory({ - id, - kind, - name: storyName, - story: getStory, - // lazily decorate the story when it's loaded - getDecorated: memoize(1)(() => this._decorateStory(getStory, decorators)), - parameters: allParam, - }); + this._storyStore.addStory( + { + id, + kind, + name: storyName, + storyFn, + parameters: allParam, + }, + { + applyDecorators: this._decorateStory, + getDecorators: () => [...localDecorators, ..._globalDecorators, withSubscriptionTracking], + } + ); return api; }; @@ -221,8 +221,8 @@ export default class ClientApi { return api; }; + // legacy getStorybook = () => - // TODO: this could all be 1 call this._storyStore.getStoryKinds().map(kind => { const fileName = this._storyStore.getStoryFileName(kind); diff --git a/lib/client-api/src/client_api.test.js b/lib/client-api/src/client_api.test.js index 20ec7c9db80b..ebf4d99b1912 100644 --- a/lib/client-api/src/client_api.test.js +++ b/lib/client-api/src/client_api.test.js @@ -123,7 +123,7 @@ describe('preview.client_api', () => { .addDecorator(fn => `aa-${fn()}`) .add('name', () => 'Hello'); - expect(storyStore.fromId('kind--name').story()).toBe('aa-Hello'); + expect(storyStore.fromId('kind--name').storyFn()).toBe('aa-Hello'); }); it('should add global decorators', () => { @@ -136,7 +136,7 @@ describe('preview.client_api', () => { storiesOf('kind', module).add('name', () => 'Hello'); - expect(storyStore.fromId('kind--name').story()).toBe('bb-Hello'); + expect(storyStore.fromId('kind--name').storyFn()).toBe('bb-Hello'); }); it('should utilize both decorators at once', () => { @@ -151,7 +151,7 @@ describe('preview.client_api', () => { .addDecorator(fn => `bb-${fn()}`) .add('name', () => 'Hello'); - expect(storyStore.fromId('kind--name').story()).toBe('aa-bb-Hello'); + expect(storyStore.fromId('kind--name').storyFn()).toBe('aa-bb-Hello'); }); it('should pass the context', () => { @@ -164,7 +164,7 @@ describe('preview.client_api', () => { .addDecorator(fn => `aa-${fn()}`) .add('name', c => `${c.kind}-${c.name}`); - const result = storyStore.fromId('kind--name').story(); + const result = storyStore.fromId('kind--name').storyFn(); expect(result).toBe(`aa-kind-name`); }); @@ -178,7 +178,7 @@ describe('preview.client_api', () => { .addDecorator((fn, { kind, name }) => `${kind}-${name}-${fn()}`) .add('name', () => 'Hello'); - const result = storyStore.fromId('kind--name').story(); + const result = storyStore.fromId('kind--name').storyFn(); expect(result).toBe(`kind-name-Hello`); }); }); @@ -252,8 +252,8 @@ describe('preview.client_api', () => { clientApi: { getStorybook, storiesOf }, } = getContext(); - const story = jest.fn(); - storiesOf('kind', { id: 'foo.js' }).add('name', story); + const fn = jest.fn(); + storiesOf('kind', { id: 'foo.js' }).add('name', fn); const storybook = getStorybook(); diff --git a/lib/client-api/src/story_store.js b/lib/client-api/src/story_store.js index bf140005c052..b7dce29d0bb0 100644 --- a/lib/client-api/src/story_store.js +++ b/lib/client-api/src/story_store.js @@ -2,10 +2,13 @@ import { history, document } from 'global'; import EventEmitter from 'eventemitter3'; import qs from 'qs'; -import Events from '@storybook/core-events'; -import { logger } from '@storybook/client-logger'; +import memoize from 'memoizerific'; import debounce from 'lodash.debounce'; import { stripIndents } from 'common-tags'; + +import Events from '@storybook/core-events'; +import { logger } from '@storybook/client-logger'; + import toId from './id'; import pathToId from './pathToId'; @@ -93,10 +96,7 @@ export default class StoryStore extends EventEmitter { return null; } - return { - ...data, - story: p => data.getDecorated()({ ...data, parameters: { ...data.parameters, ...p } }), - }; + return data; } catch (e) { logger.warn('failed to get story:', this._data); logger.error(e); @@ -134,28 +134,48 @@ export default class StoryStore extends EventEmitter { delete _data[id]; }; - addStory({ id, kind, name: storyName, story, getDecorated, parameters = {} }) { + addStory( + { id, kind, name, storyFn: original, parameters = {} }, + { getDecorators, applyDecorators } + ) { const { _data } = this; if (_data[id]) { logger.warn(stripIndents` - Story with id ${id} already exists in the store! + Story with id ${id} already exists in the store! - Perhaps you added the same story twice, or you have a name collision? - Story ids need to be unique -- ensure you aren't using the same names modolo url-sanitization.`); + Perhaps you added the same story twice, or you have a name collision? + Story ids need to be unique -- ensure you aren't using the same names modolo url-sanitization. + `); } - _data[id] = toChild({ + const identification = { + id, kind, - name: storyName, - story, + name, + story: name, // legacy + }; + + // immutable original storyFn + const getOriginal = () => original; + + // lazily decorate the story when it's loaded + const getDecorated = memoize(1)(() => applyDecorators(getOriginal(), getDecorators())); + + const storyFn = p => getDecorated()({ ...identification, parameters: { ...parameters, ...p } }); + + _data[id] = toChild({ + ...identification, + getDecorated, + getOriginal, + storyFn, + parameters, - id, }); // LEGACY DATA - this.addLegacyStory({ kind, name: storyName, story, getDecorated, parameters }); + this.addLegacyStory({ kind, name, storyFn, parameters }); // LET'S SEND IT TO THE MANAGER this.pushToManager(); @@ -179,7 +199,7 @@ export default class StoryStore extends EventEmitter { this._revision += 1; } - addLegacyStory({ kind, name, getDecorated, parameters = {} }) { + addLegacyStory({ kind, name, storyFn, parameters = {} }) { const k = toKey(kind); if (!this._legacydata[k]) { this._legacydata[k] = { @@ -194,7 +214,7 @@ export default class StoryStore extends EventEmitter { name, // kind, index: getId(), - fn: (...args) => getDecorated()(...args), + story: storyFn, parameters, }; } @@ -243,9 +263,9 @@ export default class StoryStore extends EventEmitter { return null; } - const { fn, parameters } = storyInfo; + const { story, parameters } = storyInfo; return { - story: fn, + story, parameters, }; } @@ -261,13 +281,8 @@ export default class StoryStore extends EventEmitter { return null; } - const { story, parameters } = data; - return () => - story({ - kind, - story: name, - parameters, - }); + const { story } = data; + return () => story(); } removeStoryKind(kind) { diff --git a/lib/client-api/src/story_store.test.js b/lib/client-api/src/story_store.test.js index 6b810de211ec..8eb79a840f2d 100644 --- a/lib/client-api/src/story_store.test.js +++ b/lib/client-api/src/story_store.test.js @@ -3,6 +3,7 @@ import createChannel from '@storybook/channel-postmessage'; import Events from '@storybook/core-events'; import StoryStore, { splitPath } from './story_store'; +import { defaultDecorateStory } from './client_api'; import toId from './id'; jest.mock('global', () => ({ @@ -21,22 +22,27 @@ jest.mock('global', () => ({ const channel = createChannel({ page: 'preview' }); -const make = (kind, name, story, parameters = {}) => ({ - kind, - name, - story, - getDecorated: () => story, - parameters, - id: toId(kind, name), -}); +const make = (kind, name, storyFn, parameters = {}) => [ + { + kind, + name, + storyFn, + parameters, + id: toId(kind, name), + }, + { + applyDecorators: defaultDecorateStory, + getDecorators: () => [], + }, +]; describe('preview.story_store', () => { describe('raw storage', () => { it('stores hash object', () => { const store = new StoryStore({ channel }); - store.addStory(make('a', '1', () => 0)); - store.addStory(make('a', '2', () => 0)); - store.addStory(make('b', '1', () => 0)); + store.addStory(...make('a', '1', () => 0)); + store.addStory(...make('a', '2', () => 0)); + store.addStory(...make('b', '1', () => 0)); const extracted = store.extract(); @@ -62,10 +68,10 @@ describe('preview.story_store', () => { it('should return storybook with stories', () => { const store = new StoryStore({ channel }); - store.addStory(make('kind-1', 'story-1.1', () => 0)); - store.addStory(make('kind-1', 'story-1.2', () => 0)); - store.addStory(make('kind-2', 'story-2.1', () => 0)); - store.addStory(make('kind-2', 'story-2.2', () => 0)); + store.addStory(...make('kind-1', 'story-1.1', () => 0)); + store.addStory(...make('kind-1', 'story-1.2', () => 0)); + store.addStory(...make('kind-2', 'story-2.1', () => 0)); + store.addStory(...make('kind-2', 'story-2.2', () => 0)); expect(store.dumpStoryBook()).toEqual([ { @@ -83,9 +89,9 @@ describe('preview.story_store', () => { describe('getStoryFileName', () => { it('should return the filename of the first story passed for the kind', () => { const store = new StoryStore({ channel }); - store.addStory(make('kind-1', 'story-1.1', () => 0, { fileName: 'foo.js' })); - store.addStory(make('kind-1', 'story-1.2', () => 0, { fileName: 'foo-2.js' })); - store.addStory(make('kind-2', 'story-2.1', () => 0, { fileName: 'bar.js' })); + store.addStory(...make('kind-1', 'story-1.1', () => 0, { fileName: 'foo.js' })); + store.addStory(...make('kind-1', 'story-1.2', () => 0, { fileName: 'foo-2.js' })); + store.addStory(...make('kind-2', 'story-2.1', () => 0, { fileName: 'bar.js' })); expect(store.getStoryFileName('kind-1')).toBe('foo.js'); expect(store.getStoryFileName('kind-2')).toBe('bar.js'); @@ -107,7 +113,7 @@ describe('preview.story_store', () => { fileName: 'foo.js', parameter: 'value', }; - store.addStory(make('kind', 'name', story, parameters)); + store.addStory(...make('kind', 'name', story, parameters)); expect(store.getStoryAndParameters('kind', 'name').parameters).toEqual(parameters); }); @@ -116,16 +122,18 @@ describe('preview.story_store', () => { describe('getStoryWithContext', () => { it('should return a function that calls the story with the context', () => { const store = new StoryStore({ channel }); - const story = jest.fn(); + const storyFn = jest.fn(); const parameters = { fileName: 'foo.js', parameter: 'value', }; - store.addStory(make('kind', 'name', story, parameters)); + store.addStory(...make('kind', 'name', storyFn, parameters)); const storyWithContext = store.getStoryWithContext('kind', 'name'); storyWithContext(); - expect(story).toHaveBeenCalledWith({ + expect(storyFn).toHaveBeenCalledWith({ + id: 'kind--name', + name: 'name', kind: 'kind', story: 'name', parameters, @@ -143,8 +151,7 @@ describe('preview.story_store', () => { }); describe('STORY_INIT', () => { - const story = () => 0; - const storyBundle = make('kind', 'story', story); + const storyFn = () => 0; it('supports path params', () => { document.location = { @@ -152,27 +159,28 @@ describe('preview.story_store', () => { search: '?path=/story/kind--story&bar=baz', }; const store = new StoryStore({ channel }); - store.addStory(storyBundle); + store.addStory(...make('kind', 'story', storyFn)); store.setSelection = jest.fn(); store.emit(Events.STORY_INIT); expect(history.replaceState).toHaveBeenCalledWith({}, '', 'pathname?bar=baz&id=kind--story'); expect(store.setSelection).toHaveBeenCalled(); - expect(store.setSelection.mock.calls[0][0].getDecorated()).toEqual(story); + expect(store.setSelection.mock.calls[0][0].getDecorated()).toEqual(storyFn); }); + it('supports story kind/name params', () => { document.location = { pathname: 'pathname', search: '?selectedKind=kind&selectedStory=story&bar=baz', }; const store = new StoryStore({ channel }); - store.addStory(storyBundle); + store.addStory(...make('kind', 'story', storyFn)); store.setSelection = jest.fn(); store.emit(Events.STORY_INIT); expect(history.replaceState).toHaveBeenCalledWith({}, '', 'pathname?bar=baz&id=kind--story'); expect(store.setSelection).toHaveBeenCalled(); - expect(store.setSelection.mock.calls[0][0].getDecorated()).toEqual(story); + expect(store.setSelection.mock.calls[0][0].getDecorated()).toEqual(storyFn); }); }); diff --git a/lib/core/src/client/preview/start.js b/lib/core/src/client/preview/start.js index 01d0a3b5eca3..b4da2b35267b 100644 --- a/lib/core/src/client/preview/start.js +++ b/lib/core/src/client/preview/start.js @@ -113,9 +113,10 @@ export default function start(render, { decorateStory } = {}) { const renderMain = forceRender => { const revision = storyStore.getRevision(); - const { kind, name, story, id } = storyStore.getSelection() || {}; + const selection = storyStore.getSelection(); + const { kind, name, getDecorated, id } = selection || {}; - if (story) { + if (getDecorated) { // Render story only if selectedKind or selectedStory have changed. // However, we DO want the story to re-render if the store itself has changed // (which happens at the moment when HMR occurs) @@ -140,7 +141,7 @@ export default function start(render, { decorateStory } = {}) { render({ ...context, - story, + ...selection, selectedKind: kind, selectedStory: name, forceRender,