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 (
-
- {items.map(i => (
- - {i}
- ))}
-
- );
-};
-
-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:
-
-
- );
- })
- .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 };