diff --git a/examples/angular-cli/.storybook/config.ts b/examples/angular-cli/.storybook/config.ts index 7510105da2c5..658bf0f00121 100644 --- a/examples/angular-cli/.storybook/config.ts +++ b/examples/angular-cli/.storybook/config.ts @@ -16,5 +16,4 @@ addParameters({ docs: DocsPage, }); -load(require.context('../src/stories', true, /\.stories\.ts$/), module); -load(require.context('../src/stories', true, /\.stories\.mdx$/), module); +load(require.context('../src/stories', true, /\.stories\.(ts|mdx)$/), module); diff --git a/examples/cra-kitchen-sink/.storybook/config.js b/examples/cra-kitchen-sink/.storybook/config.js index c981008d107b..af249b43a5af 100644 --- a/examples/cra-kitchen-sink/.storybook/config.js +++ b/examples/cra-kitchen-sink/.storybook/config.js @@ -22,5 +22,4 @@ addParameters({ }, }); -load(require.context('../src/stories', true, /\.stories\.js$/), module); -load(require.context('../src/stories', true, /\.stories\.mdx$/), module); +load(require.context('../src/stories', true, /\.stories\.(js|mdx)$/), module); diff --git a/examples/cra-react15/.storybook/config.js b/examples/cra-react15/.storybook/config.js index 115943f0f4f4..f68a9e50f693 100644 --- a/examples/cra-react15/.storybook/config.js +++ b/examples/cra-react15/.storybook/config.js @@ -7,4 +7,14 @@ addParameters({ }, }); -load(require.context('../src/stories', true, /\.stories\.js$/), module); +// test loading function +const loadFn = () => { + // place welcome first, test storiesof files + require('../src/stories/welcome.stories'); + + // test mixtures of storiesof & module files + const req = require.context('../src/stories', true, /\.stories\.js$/); + return req.keys().map(fname => req(fname)); +}; + +load(loadFn, module); diff --git a/examples/cra-react15/src/stories/welcome.stories.js b/examples/cra-react15/src/stories/welcome.stories.js index b32255e04085..bbb8dc8f4ba8 100644 --- a/examples/cra-react15/src/stories/welcome.stories.js +++ b/examples/cra-react15/src/stories/welcome.stories.js @@ -1,13 +1,10 @@ import React from 'react'; +import { storiesOf } from '@storybook/react'; import { linkTo } from '@storybook/addon-links'; import { Welcome } from '@storybook/react/demo'; -export default { - title: 'Welcome', - parameters: { +storiesOf('Welcome', module) + .addParameters({ component: Welcome, - }, -}; - -export const story1 = () => ; -story1.story = { name: 'to Storybook' }; + }) + .add('to Storybook', () => ); diff --git a/examples/html-kitchen-sink/.storybook/config.js b/examples/html-kitchen-sink/.storybook/config.js index cc92d650156a..0bcb2ed0e2c8 100644 --- a/examples/html-kitchen-sink/.storybook/config.js +++ b/examples/html-kitchen-sink/.storybook/config.js @@ -21,5 +21,4 @@ addParameters({ docs: DocsPage, }); -load(require.context('../stories', true, /\.stories\.js$/), module); -load(require.context('../stories', true, /\.stories\.mdx$/), module); +load(require.context('../stories', true, /\.stories\.(js|mdx)$/), module); diff --git a/examples/official-storybook/config.js b/examples/official-storybook/config.js index ae4db10692d7..6cd7f00534ee 100644 --- a/examples/official-storybook/config.js +++ b/examples/official-storybook/config.js @@ -60,7 +60,11 @@ addParameters({ docs: DocsPage, }); -load(require.context('../../lib/ui/src', true, /\.stories\.js$/), module); -load(require.context('../../lib/components/src', true, /\.stories\.tsx?$/), module); -load(require.context('./stories', true, /\.stories\.js$/), module); -load(require.context('./stories', true, /\.stories\.mdx$/), module); +load( + [ + require.context('../../lib/ui/src', true, /\.stories\.(js|tsx?|mdx)$/), + require.context('../../lib/components/src', true, /\.stories\.(js|tsx?|mdx)$/), + require.context('./stories', true, /\.stories\.(js|tsx?|mdx)$/), + ], + module +); diff --git a/examples/official-storybook/stories/addon-knobs.stories.js b/examples/official-storybook/stories/addon-knobs.stories.js deleted file mode 100644 index 25622355aacd..000000000000 --- a/examples/official-storybook/stories/addon-knobs.stories.js +++ /dev/null @@ -1,331 +0,0 @@ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { storiesOf } from '@storybook/react'; - -import { - withKnobs, - text, - number, - boolean, - color, - select, - radios, - array, - date, - button, - object, - files, - optionsKnob as options, -} from '@storybook/addon-knobs'; - -const ItemLoader = ({ isLoading, items }) => { - if (isLoading) { - return

Loading data

; - } - if (!items.length) { - return

No items loaded

; - } - return ( - - ); -}; - -ItemLoader.propTypes = { - isLoading: PropTypes.bool.isRequired, - items: PropTypes.arrayOf(PropTypes.string).isRequired, -}; -let injectedItems = []; -let injectedIsLoading = false; - -storiesOf('Addons|Knobs.withKnobs', module) - .addDecorator(withKnobs) - .add('tweaks static values', () => { - const name = text('Name', 'Storyteller'); - const age = number('Age', 70, { range: true, min: 0, max: 90, step: 5 }); - const fruits = { - Apple: 'apple', - Banana: 'banana', - Cherry: 'cherry', - }; - const fruit = select('Fruit', fruits, 'apple'); - - const otherFruits = { - Kiwi: 'kiwi', - Guava: 'guava', - Watermelon: 'watermelon', - }; - const otherFruit = radios('Other Fruit', otherFruits, 'watermelon'); - const dollars = number('Dollars', 12.5, { min: 0, max: 100, step: 0.01 }); - const years = number('Years in NY', 9); - - const backgroundColor = color('background', '#dedede'); - const items = array('Items', ['Laptop', 'Book', 'Whiskey']); - const otherStyles = object('Styles', { - border: '2px dashed silver', - borderRadius: 10, - padding: '10px', - }); - const nice = boolean('Nice', true); - const images = files('Happy Picture', 'image/*', [ - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfiARwMCyEWcOFPAAAAP0lEQVQoz8WQMQoAIAwDL/7/z3GwghSp4KDZyiUpBMCYUgd8rehtH16/l3XewgU2KAzapjXBbNFaPS6lDMlKB6OiDv3iAH1OAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE4LTAxLTI4VDEyOjExOjMzLTA3OjAwlAHQBgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOC0wMS0yOFQxMjoxMTozMy0wNzowMOVcaLoAAAAASUVORK5CYII=', - ]); - - // NOTE: the default value must not change - e.g., do not do date('Label', new Date()) or date('Label') - const defaultBirthday = new Date('Jan 20 2017 GMT+0'); - const birthday = date('Birthday', defaultBirthday); - - const intro = `My name is ${name}, I'm ${age} years old, and my favorite fruit is ${fruit}. I also enjoy ${otherFruit}.`; - const style = { backgroundColor, ...otherStyles }; - const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!'; - const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }; - - return ( -
-

{intro}

-

My birthday is: {new Date(birthday).toLocaleDateString('en-US', dateOptions)}

-

I live in NY for {years} years.

-

My wallet contains: ${dollars.toFixed(2)}

-

In my backpack, I have:

-
    - {items.map(item => ( -
  • {item}
  • - ))} -
-

{salutation}

-

- When I am happy I look like this: happy -

-
- ); - }) - .add('tweaks static values organized in groups', () => { - const GROUP_IDS = { - DISPLAY: 'Display', - GENERAL: 'General', - FAVORITES: 'Favorites', - }; - - const fruits = { - Apple: 'apple', - Banana: 'banana', - Cherry: 'cherry', - }; - - const otherFruits = { - Kiwi: 'kiwi', - Guava: 'guava', - Watermelon: 'watermelon', - }; - - // NOTE: the default value must not change - e.g., do not do date('Label', new Date()) or date('Label') - const defaultBirthday = new Date('Jan 20 2017 GMT+0'); - - // Ungrouped - const ungrouped = text('Ungrouped', 'Mumble'); - - // General - const name = text('Name', 'Storyteller', GROUP_IDS.GENERAL); - const age = number('Age', 70, { range: true, min: 0, max: 90, step: 5 }, GROUP_IDS.GENERAL); - const birthday = date('Birthday', defaultBirthday, GROUP_IDS.GENERAL); - const dollars = number( - 'Account Balance', - 12.5, - { min: 0, max: 100, step: 0.01 }, - GROUP_IDS.GENERAL - ); - const years = number('Years in NY', 9, {}, GROUP_IDS.GENERAL); - const generalNotes = text('Notes', '', GROUP_IDS.GENERAL); - - // Favorites - const nice = boolean('Nice', true, GROUP_IDS.FAVORITES); - const fruit = select('Fruit', fruits, 'apple', GROUP_IDS.FAVORITES); - const otherFruit = radios('Other Fruit', otherFruits, 'watermelon', GROUP_IDS.FAVORITES); - const items = array('Items', ['Laptop', 'Book', 'Whiskey'], ',', GROUP_IDS.FAVORITES); - const favoritesNotes = text('Notes', '', GROUP_IDS.FAVORITES); - - // Display - const backgroundColor = color('Color', 'rgba(126, 211, 33, 0.22)', GROUP_IDS.DISPLAY); - const otherStyles = object( - 'Styles', - { - border: '2px dashed silver', - borderRadius: 10, - padding: '10px', - }, - GROUP_IDS.DISPLAY - ); - - const style = { backgroundColor, ...otherStyles }; - - const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!'; - const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }; - - return ( -
-

General Information

-

Name: {name}

-

Age: {age}

-

Birthday: {new Date(birthday).toLocaleDateString('en-US', dateOptions)}

-

Account Balance: {dollars}

-

Years in NY: {years}

-

Notes: {generalNotes}

-
-

Favorites

-

Catchphrase: {salutation}

-

Fruit: {fruit}

-

Other Fruit: {otherFruit}

-

Items:

-
    - {items.map(item => ( -
  • {item}
  • - ))} -
-

When I'm by myself, I say: "{ungrouped}"

-

Notes: {favoritesNotes}

-
- ); - }) - .add('dynamic knobs', () => { - const showOptional = select('Show optional', ['yes', 'no'], 'yes'); - return ( - -
{text('compulsory', 'I must be here')}
- {showOptional === 'yes' ?
{text('optional', 'I can disappear')}
: null} -
- ); - }) - .add('complex select', () => { - const m = select( - 'complex', - { - number: 1, - string: 'string', - object: {}, - array: [1, 2, 3], - function: () => {}, - }, - 'string' - ); - const value = m.toString(); - const type = Array.isArray(m) ? 'array' : typeof m; - return ( -
-        the type of {JSON.stringify(value, null, 2)} = {type}
-      
- ); - }) - .add('optionsKnob', () => { - const valuesRadio = { - Monday: 'Monday', - Tuesday: 'Tuesday', - Wednesday: 'Wednesday', - }; - const optionRadio = options('Radio', valuesRadio, 'Tuesday', { display: 'radio' }); - - const valuesInlineRadio = { - Saturday: 'Saturday', - Sunday: 'Sunday', - }; - const optionInlineRadio = options('Inline Radio', valuesInlineRadio, 'Saturday', { - display: 'inline-radio', - }); - - const valuesSelect = { - January: 'January', - February: 'February', - March: 'March', - }; - const optionSelect = options('Select', valuesSelect, 'January', { display: 'select' }); - - const valuesMultiSelect = { - Apple: 'apple', - Banana: 'banana', - Cherry: 'cherry', - }; - const optionsMultiSelect = options('Multi Select', valuesMultiSelect, ['apple'], { - display: 'multi-select', - }); - - const valuesCheck = { - Corn: 'corn', - Carrot: 'carrot', - Cucumber: 'cucumber', - }; - const optionsCheck = options('Check', valuesCheck, ['carrot'], { display: 'check' }); - - const valuesInlineCheck = { - Milk: 'milk', - Cheese: 'cheese', - Butter: 'butter', - }; - const optionsInlineCheck = options('Inline Check', valuesInlineCheck, ['milk'], { - display: 'inline-check', - }); - - return ( -
-

Weekday: {optionRadio}

-

Weekend: {optionInlineRadio}

-

Month: {optionSelect}

-

Fruit:

-
    - {optionsMultiSelect.map(item => ( -
  • {item}
  • - ))} -
-

Vegetables:

-
    - {optionsCheck.map(item => ( -
  • {item}
  • - ))} -
-

Dairy:

-
    - {optionsInlineCheck.map(item => ( -
  • {item}
  • - ))} -
-
- ); - }) - .add('triggers actions via button', () => { - button('Toggle item list state', () => { - if (!injectedIsLoading && injectedItems.length === 0) { - injectedIsLoading = true; - } else if (injectedIsLoading && injectedItems.length === 0) { - injectedIsLoading = false; - injectedItems = ['pencil', 'pen', 'eraser']; - } else if (injectedItems.length > 0) { - injectedItems = []; - } - }); - return ( - -

Hit the knob button and it will toggle the items list into multiple states.

- -
- ); - }) - .add('XSS safety', () => ( -
'), - }} - /> - )) - .add('accepts story parameters', () =>
{text('Rendered string', '

Hello

')}
, { - knobs: { escapeHTML: false }, - }); - -storiesOf('Addons|Knobs.withKnobs using options', module) - .addDecorator( - withKnobs({ - escapeHTML: false, - }) - ) - .add('accepts options', () =>
{text('Rendered string', '

Hello

')}
); diff --git a/examples/official-storybook/stories/addon-notes.stories.js b/examples/official-storybook/stories/addon-notes.stories.js index 178497d939c1..60c57fc71cea 100644 --- a/examples/official-storybook/stories/addon-notes.stories.js +++ b/examples/official-storybook/stories/addon-notes.stories.js @@ -3,10 +3,6 @@ import React from 'react'; import BaseButton from '../components/BaseButton'; import markdownNotes from './notes/notes.md'; -const baseStory = () => ( - -); - const markdownString = ` # Documentation @@ -75,52 +71,57 @@ export default { title: 'Addons|Notes', }; -export const addonNotes = baseStory; +export const addonNotes = () => ( + +); addonNotes.story = { name: 'addon notes', - parameters: { notes: 'This is the notes for a button. This is helpful for adding details about a story in a separate panel.', }, }; -export const addonNotesRenderingImportedMarkdown = baseStory; +export const addonNotesRenderingImportedMarkdown = () => ( + +); addonNotesRenderingImportedMarkdown.story = { name: 'addon notes rendering imported markdown', - parameters: { notes: { markdown: markdownNotes }, }, }; -export const addonNotesRenderingInlineGithubFlavoredMarkdown = baseStory; +export const addonNotesRenderingInlineGithubFlavoredMarkdown = () => ( + +); addonNotesRenderingInlineGithubFlavoredMarkdown.story = { name: 'addon notes rendering inline, github-flavored markdown', - parameters: { notes: { markdown: markdownString }, }, }; -export const withAMarkdownTable = baseStory; +export const withAMarkdownTable = () => ( + +); withAMarkdownTable.story = { name: 'with a markdown table', - parameters: { notes: { markdown: markdownTable }, }, }; -export const withAMarkdownGiphy = baseStory; +export const withAMarkdownGiphy = () => ( + +); withAMarkdownGiphy.story = { name: 'with a markdown giphy', - parameters: { notes: { markdown: giphyMarkdown }, }, diff --git a/examples/vue-kitchen-sink/.storybook/config.js b/examples/vue-kitchen-sink/.storybook/config.js index 9fbd5064f928..fd61b0f97bc4 100644 --- a/examples/vue-kitchen-sink/.storybook/config.js +++ b/examples/vue-kitchen-sink/.storybook/config.js @@ -20,5 +20,4 @@ addParameters({ docs: DocsPage, }); -load(require.context('../src/stories', true, /\.stories\.js$/), module); -load(require.context('../src/stories', true, /\.stories\.mdx$/), module); +load(require.context('../src/stories', true, /\.stories\.(js|mdx)$/), module); diff --git a/lib/client-api/src/story_store.ts b/lib/client-api/src/story_store.ts index 3292de148772..f70c1e972056 100644 --- a/lib/client-api/src/story_store.ts +++ b/lib/client-api/src/story_store.ts @@ -334,6 +334,7 @@ export default class StoryStore extends EventEmitter { } return acc; }, {}); + this.pushToManager(); } } diff --git a/lib/core/src/client/preview/start.js b/lib/core/src/client/preview/start.js index 39044742faee..e49d770f7585 100644 --- a/lib/core/src/client/preview/start.js +++ b/lib/core/src/client/preview/start.js @@ -304,11 +304,40 @@ export default function start(render, { decorateStory } = {}) { window.__STORYBOOK_ADDONS_CHANNEL__ = channel; // may not be defined } - let previousExports = {}; - const loadStories = (req, framework) => () => { - req.keys().forEach(filename => { - const fileExports = req(filename); + let previousExports = new Set(); + const loadStories = (loadable, framework) => () => { + let reqs = null; + if (Array.isArray(loadable)) { + reqs = loadable; + } else if (loadable.keys) { + reqs = [loadable]; + } + + let currentExports; + if (reqs) { + currentExports = new Set(); + reqs.forEach(req => { + req.keys().forEach(filename => { + const fileExports = req(filename); + currentExports.add(fileExports); + }); + }); + } else { + currentExports = new Set(loadable()); + } + + const removed = [...previousExports].filter(exp => !currentExports.has(exp)); + removed.forEach(exp => { + if (exp.default) { + storyStore.removeStoryKind(exp.default.title); + } + }); + if (removed.length > 1) { + storyStore.incrementRevision(); + } + const added = [...currentExports].filter(exp => !previousExports.has(exp)); + added.forEach(fileExports => { // An old-style story file if (!fileExports.default) { return; @@ -316,25 +345,13 @@ export default function start(render, { decorateStory } = {}) { if (!fileExports.default.title) { throw new Error( - `Unexpected default export without title in '${filename}': ${JSON.stringify( - fileExports.default - )}` + `Unexpected default export without title: ${JSON.stringify(fileExports.default)}` ); } const { default: meta, ...exports } = fileExports; const kindName = meta.title; - if (previousExports[filename]) { - if (previousExports[filename] === fileExports) { - return; - } - - // Otherwise clear this kind - storyStore.removeStoryKind(kindName); - storyStore.incrementRevision(); - } - // We pass true here to avoid the warning about HMR. It's cool clientApi, we got this const kind = clientApi.storiesOf(kindName, true); kind.addParameters({ framework }); @@ -358,21 +375,35 @@ export default function start(render, { decorateStory } = {}) { kind.add(name || key, storyFn, { ...parameters, ...decoratorParams }); } }); - - previousExports[filename] = fileExports; }); + previousExports = currentExports; }; - const load = (req, m, framework) => { + let loaded = false; + /** + * Load a collection of stories. If it has a default export, assume that it is a module-style + * file and process its named exports as stories. If not, assume it's an old-style + * storiesof file and simply require it. + * + * @param {*} loadable a require.context `req`, an array of `req`s, or a loader function that returns void or an array of exports + * @param {*} m - ES module object for hot-module-reloading (HMR) + * @param {*} framework - name of framework in use, e.g. "react" + */ + const load = (loadable, m, framework) => { if (m && m.hot && m.hot.dispose) { - ({ previousExports = {} } = m.hot.data || {}); + ({ previousExports = new Set() } = m.hot.data || {}); m.hot.dispose(data => { + loaded = false; // eslint-disable-next-line no-param-reassign data.previousExports = previousExports; }); } - configApi.configure(loadStories(req, framework), m); + if (loaded) { + logger.warn('Unexpected loaded state. Did you call `load` twice?'); + } + loaded = true; + configApi.configure(loadStories(loadable, framework), m); }; return { load, context, clientApi, configApi, forceReRender };