Merge branch 'next' into 7101-source-loader

This commit is contained in:
Michael Shilman 2019-06-25 17:53:16 +08:00
commit ca2dc93f54
23 changed files with 291 additions and 72 deletions

View File

@ -29,7 +29,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true; MDXContent.isMDXComponent = true;
const componentMeta = {}; const componentMeta = { includeStories: [] };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => ( const WrappedMDXContent = ({ context }) => (
@ -105,6 +105,7 @@ const componentMeta = {
</div> </div>
), ),
], ],
includeStories: ['one'],
}; };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
@ -118,6 +119,65 @@ export default componentMeta;
" "
`; `;
exports[`docs-mdx-compiler-plugin supports non-story exports 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
export const two = 2;
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {
two,
};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Button\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"one\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">One</Button>
</Story>
<Story name=\\"hello story\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.parameters = { mdxSource: \`<Button>One</Button>\` };
export const helloStory = () => <Button>Hello button</Button>;
helloStory.title = 'hello story';
helloStory.parameters = { mdxSource: \`<Button>Hello button</Button>\` };
const componentMeta = { title: 'Button', includeStories: ['one', 'helloStory'] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
<DocsContainer context={{ ...context, mdxKind }} content={MDXContent} />
);
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = WrappedMDXContent;
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin supports object-style story definitions 1`] = ` exports[`docs-mdx-compiler-plugin supports object-style story definitions 1`] = `
"/* @jsx mdx */ "/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks'; import { DocsContainer } from '@storybook/addon-docs/blocks';
@ -182,7 +242,7 @@ toStorybook.parameters = {
}\`, }\`,
}; };
const componentMeta = { title: 'MDX|Welcome' }; const componentMeta = { title: 'MDX|Welcome', includeStories: ['toStorybook'] };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => ( const WrappedMDXContent = ({ context }) => (
@ -262,6 +322,7 @@ const componentMeta = {
component: Button, component: Button,
notes: 'component notes', notes: 'component notes',
}, },
includeStories: ['componentNotes', 'storyNotes'],
}; };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
@ -336,6 +397,7 @@ const componentMeta = {
component: Button, component: Button,
notes: 'component notes', notes: 'component notes',
}, },
includeStories: ['helloButton', 'two'],
}; };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
@ -392,7 +454,7 @@ export const helloStory = () => <Button>Hello button</Button>;
helloStory.title = 'hello story'; helloStory.title = 'hello story';
helloStory.parameters = { mdxSource: \`<Button>Hello button</Button>\` }; helloStory.parameters = { mdxSource: \`<Button>Hello button</Button>\` };
const componentMeta = { title: 'Button' }; const componentMeta = { title: 'Button', includeStories: ['one', 'helloStory'] };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => ( const WrappedMDXContent = ({ context }) => (
@ -434,7 +496,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true; MDXContent.isMDXComponent = true;
const componentMeta = {}; const componentMeta = { includeStories: [] };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => ( const WrappedMDXContent = ({ context }) => (
@ -482,7 +544,7 @@ MDXContent.isMDXComponent = true;
export const text = () => 'Plain text'; export const text = () => 'Plain text';
text.parameters = { mdxSource: \`'Plain text'\` }; text.parameters = { mdxSource: \`'Plain text'\` };
const componentMeta = { title: 'Text' }; const componentMeta = { title: 'Text', includeStories: ['text'] };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => ( const WrappedMDXContent = ({ context }) => (
@ -525,7 +587,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true; MDXContent.isMDXComponent = true;
const componentMeta = {}; const componentMeta = { includeStories: [] };
const mdxKind = componentMeta.title || componentMeta.displayName; const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => ( const WrappedMDXContent = ({ context }) => (

View File

@ -0,0 +1,16 @@
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
<Meta title="Button" />
# Story definition
<Story name="one">
<Button>One</Button>
</Story>
export const two = 2;
<Story name="hello story">
<Button>Hello button</Button>
</Story>

View File

@ -44,7 +44,7 @@ function genStoryExport(ast, counter) {
// console.log('genStoryExport', JSON.stringify(ast, null, 2)); // console.log('genStoryExport', JSON.stringify(ast, null, 2));
const statements = []; const statements = [];
const storyFn = getStoryFn(storyName, counter); const storyKey = getStoryFn(storyName, counter);
let body = ast.children.find(n => n.type !== 'JSXText'); let body = ast.children.find(n => n.type !== 'JSXText');
let storyCode = null; let storyCode = null;
@ -61,13 +61,13 @@ function genStoryExport(ast, counter) {
storyCode = code; storyCode = code;
} }
statements.push( statements.push(
`export const ${storyFn} = () => ( `export const ${storyKey} = () => (
${storyCode} ${storyCode}
);` );`
); );
if (storyName !== storyFn) { if (storyName !== storyKey) {
statements.push(`${storyFn}.title = '${storyName}';`); statements.push(`${storyKey}.title = '${storyName}';`);
} }
let parameters = getAttr(ast.openingElement, 'parameters'); let parameters = getAttr(ast.openingElement, 'parameters');
@ -76,27 +76,29 @@ function genStoryExport(ast, counter) {
if (parameters) { if (parameters) {
const { code: params } = generate(parameters, {}); const { code: params } = generate(parameters, {});
// FIXME: hack in the story's source as a parameter // FIXME: hack in the story's source as a parameter
statements.push(`${storyFn}.parameters = { mdxSource: ${source}, ...${params} };`); statements.push(`${storyKey}.parameters = { mdxSource: ${source}, ...${params} };`);
} else { } else {
statements.push(`${storyFn}.parameters = { mdxSource: ${source} };`); statements.push(`${storyKey}.parameters = { mdxSource: ${source} };`);
} }
// console.log(statements); // console.log(statements);
return [statements.join('\n')]; return {
[storyKey]: statements.join('\n'),
};
} }
function genPreviewExports(ast, counter) { function genPreviewExports(ast, counter) {
// console.log('genPreviewExports', JSON.stringify(ast, null, 2)); // console.log('genPreviewExports', JSON.stringify(ast, null, 2));
let localCounter = counter; let localCounter = counter;
const previewExports = []; const previewExports = {};
for (let i = 0; i < ast.children.length; i += 1) { for (let i = 0; i < ast.children.length; i += 1) {
const child = ast.children[i]; const child = ast.children[i];
if (child.type === 'JSXElement' && child.openingElement.name.name === 'Story') { if (child.type === 'JSXElement' && child.openingElement.name.name === 'Story') {
const storyExport = genStoryExport(child, localCounter); const storyExport = genStoryExport(child, localCounter);
if (storyExport) { if (storyExport) {
previewExports.push(storyExport); Object.assign(previewExports, storyExport);
localCounter += 1; localCounter += 1;
} }
} }
@ -104,20 +106,24 @@ function genPreviewExports(ast, counter) {
return previewExports; return previewExports;
} }
function genMetaExport(ast) { function genMeta(ast) {
let title = getAttr(ast.openingElement, 'title'); let title = getAttr(ast.openingElement, 'title');
let parameters = getAttr(ast.openingElement, 'parameters'); let parameters = getAttr(ast.openingElement, 'parameters');
let decorators = getAttr(ast.openingElement, 'decorators'); let decorators = getAttr(ast.openingElement, 'decorators');
title = title && `title: '${title.value}',`; title = title && `'${title.value}'`;
if (parameters && parameters.expression) { if (parameters && parameters.expression) {
const { code: params } = generate(parameters.expression, {}); const { code: params } = generate(parameters.expression, {});
parameters = `parameters: ${params},`; parameters = params;
} }
if (decorators && decorators.expression) { if (decorators && decorators.expression) {
const { code: decos } = generate(decorators.expression, {}); const { code: decos } = generate(decorators.expression, {});
decorators = `decorators: ${decos},`; decorators = decos;
} }
return `const componentMeta = { ${title || ''} ${parameters || ''} ${decorators || ''} };`; return {
title,
parameters,
decorators,
};
} }
function getExports(node, counter) { function getExports(node, counter) {
@ -127,7 +133,7 @@ function getExports(node, counter) {
// Single story // Single story
const ast = parser.parseExpression(value, { plugins: ['jsx'] }); const ast = parser.parseExpression(value, { plugins: ['jsx'] });
const storyExport = genStoryExport(ast, counter); const storyExport = genStoryExport(ast, counter);
return storyExport && { stories: [storyExport] }; return storyExport && { stories: storyExport };
} }
if (PREVIEW_REGEX.exec(value)) { if (PREVIEW_REGEX.exec(value)) {
// Preview, possibly containing multiple stories // Preview, possibly containing multiple stories
@ -137,7 +143,7 @@ function getExports(node, counter) {
if (META_REGEX.exec(value)) { if (META_REGEX.exec(value)) {
// Preview, possibly containing multiple stories // Preview, possibly containing multiple stories
const ast = parser.parseExpression(value, { plugins: ['jsx'] }); const ast = parser.parseExpression(value, { plugins: ['jsx'] });
return { meta: genMetaExport(ast) }; return { meta: genMeta(ast) };
} }
} }
return null; return null;
@ -152,10 +158,22 @@ componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = WrappedMDXContent; componentMeta.parameters.docs = WrappedMDXContent;
`.trim(); `.trim();
function stringifyMeta(meta) {
let result = '{ ';
Object.entries(meta).forEach(([key, val]) => {
if (val) {
result += `${key}: ${val}, `;
}
});
result += ' }';
return result;
}
function extractExports(node, options) { function extractExports(node, options) {
// we're overriding default export // we're overriding default export
const defaultJsx = mdxToJsx.toJSX(node, {}, { ...options, skipExport: true }); const defaultJsx = mdxToJsx.toJSX(node, {}, { ...options, skipExport: true });
const storyExports = []; const storyExports = [];
const includeStories = [];
let metaExport = null; let metaExport = null;
let counter = 0; let counter = 0;
node.children.forEach(n => { node.children.forEach(n => {
@ -163,7 +181,8 @@ function extractExports(node, options) {
if (exports) { if (exports) {
const { stories, meta } = exports; const { stories, meta } = exports;
if (stories) { if (stories) {
stories.forEach(story => { Object.entries(stories).forEach(([key, story]) => {
includeStories.push(key);
storyExports.push(story); storyExports.push(story);
counter += 1; counter += 1;
}); });
@ -177,14 +196,15 @@ function extractExports(node, options) {
} }
}); });
if (!metaExport) { if (!metaExport) {
metaExport = 'const componentMeta = { };'; metaExport = {};
} }
metaExport.includeStories = JSON.stringify(includeStories);
const fullJsx = [ const fullJsx = [
'import { DocsContainer } from "@storybook/addon-docs/blocks";', 'import { DocsContainer } from "@storybook/addon-docs/blocks";',
defaultJsx, defaultJsx,
...storyExports, ...storyExports,
metaExport, `const componentMeta = ${stringifyMeta(metaExport)};`,
wrapperJs, wrapperJs,
'export default componentMeta;', 'export default componentMeta;',
].join('\n\n'); ].join('\n\n');

View File

@ -63,6 +63,10 @@ describe('docs-mdx-compiler-plugin', () => {
const code = await generate(path.resolve(__dirname, './fixtures/parameters.mdx')); const code = await generate(path.resolve(__dirname, './fixtures/parameters.mdx'));
expect(code).toMatchSnapshot(); expect(code).toMatchSnapshot();
}); });
it('supports non-story exports', async () => {
const code = await generate(path.resolve(__dirname, './fixtures/non-story-exports.mdx'));
expect(code).toMatchSnapshot();
});
it('errors on missing story props', async () => { it('errors on missing story props', async () => {
await expect( await expect(
generate(path.resolve(__dirname, './fixtures/story-missing-props.mdx')) generate(path.resolve(__dirname, './fixtures/story-missing-props.mdx'))

View File

@ -51,6 +51,10 @@ import { Button } from '@storybook/react/demo';
<Button>just a button, not a story</Button> <Button>just a button, not a story</Button>
export const nonStory1 = 'foo'; // a non-story export
export const nonStory2 = () => <Button>Not a story</Button>; // another one
<Preview> <Preview>
<Story name="hello story"> <Story name="hello story">
<Button onClick={action('clicked')}>hello world</Button> <Button onClick={action('clicked')}>hello world</Button>
@ -67,6 +71,10 @@ import { Button } from '@storybook/react/demo';
<Story name="plaintext">Plain text</Story> <Story name="plaintext">Plain text</Story>
</Preview> </Preview>
<Story name="solo story">
<Button onClick={action('clicked')}>solo</Button>
</Story>
<Source name="hello story" /> <Source name="hello story" />
## Configurable height ## Configurable height

View File

@ -69,6 +69,9 @@ module.exports = {
'/dll/', '/dll/',
'/__mocks__ /', '/__mocks__ /',
], ],
globals: {
DOCS_MODE: false,
},
snapshotSerializers: ['jest-emotion', 'enzyme-to-json/serializer'], snapshotSerializers: ['jest-emotion', 'enzyme-to-json/serializer'],
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
setupFilesAfterEnv: ['./scripts/jest.init.js'], setupFilesAfterEnv: ['./scripts/jest.init.js'],

View File

@ -43,7 +43,10 @@ const { STORY_CHANGED, SET_STORIES, SELECT_STORY } = Events;
export type Module = StoreData & export type Module = StoreData &
RouterData & RouterData &
ProviderData & { mode?: 'production' | 'development' }; ProviderData & {
mode?: 'production' | 'development';
state: State;
};
export type State = Other & export type State = Other &
LayoutSubState & LayoutSubState &
@ -82,6 +85,10 @@ interface ProviderData {
provider: Provider; provider: Provider;
} }
interface DocsModeData {
docsMode: boolean;
}
interface StoreData { interface StoreData {
store: Store; store: Store;
} }
@ -92,7 +99,7 @@ interface Children {
type StatePartial = Partial<State>; type StatePartial = Partial<State>;
export type Props = Children & RouterData & ProviderData; export type Props = Children & RouterData & ProviderData & DocsModeData;
class ManagerProvider extends Component<Props, State> { class ManagerProvider extends Component<Props, State> {
static displayName = 'Manager'; static displayName = 'Manager';
@ -103,26 +110,41 @@ class ManagerProvider extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
const { provider, location, path, viewMode, storyId, navigate } = props; const {
provider,
location,
path,
viewMode = props.docsMode ? 'docs' : 'story',
storyId,
docsMode,
navigate,
} = props;
const store = new Store({ const store = new Store({
getState: () => this.state, getState: () => this.state,
setState: (stateChange: StatePartial, callback) => this.setState(stateChange, callback), setState: (stateChange: StatePartial, callback) => this.setState(stateChange, callback),
}); });
const routeData = { location, path, viewMode, storyId };
// Initialize the state to be the initial (persisted) state of the store. // Initialize the state to be the initial (persisted) state of the store.
// This gives the modules the chance to read the persisted state, apply their defaults // This gives the modules the chance to read the persisted state, apply their defaults
// and override if necessary // and override if necessary
this.state = store.getInitialState(getInitialState({})); const docsModeState = {
layout: { isToolshown: false, showPanel: false },
ui: { docsMode: true },
};
this.state = store.getInitialState(
getInitialState({
...routeData,
...(docsMode ? docsModeState : null),
})
);
const apiData = { const apiData = {
navigate, navigate,
store, store,
provider, provider,
location,
path,
viewMode,
storyId,
}; };
this.modules = [ this.modules = [
@ -134,7 +156,7 @@ class ManagerProvider extends Component<Props, State> {
initStories, initStories,
initURL, initURL,
initVersions, initVersions,
].map(initModule => initModule(apiData)); ].map(initModule => initModule({ ...routeData, ...apiData, state: this.state }));
// Create our initial state by combining the initial state of all modules, then overlaying any saved state // Create our initial state by combining the initial state of all modules, then overlaying any saved state
const state = getInitialState(...this.modules.map(m => m.state)); const state = getInitialState(...this.modules.map(m => m.state));

View File

@ -24,6 +24,7 @@ export interface UI {
url?: string; url?: string;
enableShortcuts: boolean; enableShortcuts: boolean;
sidebarAnimations: boolean; sidebarAnimations: boolean;
docsMode: boolean;
} }
export interface SubState { export interface SubState {
@ -132,6 +133,7 @@ const initial: SubState = {
ui: { ui: {
enableShortcuts: true, enableShortcuts: true,
sidebarAnimations: true, sidebarAnimations: true,
docsMode: false,
}, },
layout: { layout: {
isToolshown: true, isToolshown: true,

View File

@ -10,6 +10,7 @@ interface Additions {
panelPosition?: PanelPositions; panelPosition?: PanelPositions;
showNav?: boolean; showNav?: boolean;
selectedPanel?: string; selectedPanel?: string;
viewMode?: string;
} }
// Initialize the state based on the URL. // Initialize the state based on the URL.
@ -21,7 +22,7 @@ interface Additions {
// - nav: 0/1 -- show or hide the story list // - nav: 0/1 -- show or hide the story list
// //
// We also support legacy URLs from storybook <5 // We also support legacy URLs from storybook <5
const initialUrlSupport = ({ navigate, location, path }: Module) => { const initialUrlSupport = ({ navigate, state: { location, path, viewMode, storyId } }: Module) => {
const addition: Additions = {}; const addition: Additions = {};
const query = queryFromLocation(location); const query = queryFromLocation(location);
let selectedPanel; let selectedPanel;
@ -70,20 +71,20 @@ const initialUrlSupport = ({ navigate, location, path }: Module) => {
} }
if (selectedKind && selectedStory) { if (selectedKind && selectedStory) {
const storyId = toId(selectedKind, selectedStory); const id = toId(selectedKind, selectedStory);
setTimeout(() => navigate(`/story/${storyId}`, { replace: true }), 1); setTimeout(() => navigate(`/${viewMode}/${id}`, { replace: true }), 1);
} else if (selectedKind) { } else if (selectedKind) {
// Create a "storyId" of the form `kind-sanitized--*` // Create a "storyId" of the form `kind-sanitized--*`
const standInId = toId(selectedKind, 'star').replace(/star$/, '*'); const standInId = toId(selectedKind, 'star').replace(/star$/, '*');
setTimeout(() => navigate(`/story/${standInId}`, { replace: true }), 1); setTimeout(() => navigate(`/${viewMode}/${standInId}`, { replace: true }), 1);
} else if (!queryPath || queryPath === '/') { } else if (!queryPath || queryPath === '/') {
setTimeout(() => navigate(`/story/*`, { replace: true }), 1); setTimeout(() => navigate(`/${viewMode}/*`, { replace: true }), 1);
} else if (Object.keys(query).length > 1) { } else if (Object.keys(query).length > 1) {
// remove other queries // remove other queries
setTimeout(() => navigate(`${queryPath}`, { replace: true }), 1); setTimeout(() => navigate(`${queryPath}`, { replace: true }), 1);
} }
return { layout: addition, selectedPanel, location, path, customQueryParams }; return { viewMode, layout: addition, selectedPanel, location, path, customQueryParams, storyId };
}; };
export interface QueryParams { export interface QueryParams {
@ -102,7 +103,7 @@ export interface SubAPI {
setQueryParams: (input: QueryParams) => void; setQueryParams: (input: QueryParams) => void;
} }
export default function({ store, navigate, location, path: initialPath, ...rest }: Module) { export default function({ store, navigate, state, provider, ...rest }: Module) {
const api: SubAPI = { const api: SubAPI = {
getQueryParam: key => { getQueryParam: key => {
const { customQueryParams } = store.getState(); const { customQueryParams } = store.getState();
@ -142,6 +143,6 @@ export default function({ store, navigate, location, path: initialPath, ...rest
return { return {
api, api,
state: initialUrlSupport({ store, navigate, location, path: initialPath, ...rest }), state: initialUrlSupport({ store, navigate, state, provider, ...rest }),
}; };
} }

View File

@ -17,6 +17,7 @@ describe('layout API', () => {
ui: { ui: {
enableShortcuts: true, enableShortcuts: true,
sidebarAnimations: true, sidebarAnimations: true,
docsMode: false,
}, },
layout: { layout: {
isToolshown: true, isToolshown: true,

View File

@ -5,12 +5,14 @@ import initURL from '../modules/url';
jest.useFakeTimers(); jest.useFakeTimers();
describe('initial state', () => { describe('initial state', () => {
const viewMode = 'story';
it('redirects to /story/* if path is blank', () => { it('redirects to /story/* if path is blank', () => {
const navigate = jest.fn(); const navigate = jest.fn();
const location = { search: null }; const location = { search: null };
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location, viewMode } });
// Nothing unexpected in layout // Nothing unexpected in layout
expect(layout).toEqual({}); expect(layout).toEqual({});
@ -25,7 +27,7 @@ describe('initial state', () => {
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ isFullscreen: true }); expect(layout).toEqual({ isFullscreen: true });
}); });
@ -36,7 +38,7 @@ describe('initial state', () => {
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ showNav: false }); expect(layout).toEqual({ showNav: false });
}); });
@ -47,7 +49,7 @@ describe('initial state', () => {
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ panelPosition: 'bottom' }); expect(layout).toEqual({ panelPosition: 'bottom' });
}); });
@ -58,7 +60,7 @@ describe('initial state', () => {
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ panelPosition: 'right' }); expect(layout).toEqual({ panelPosition: 'right' });
}); });
@ -69,7 +71,7 @@ describe('initial state', () => {
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ showPanel: false }); expect(layout).toEqual({ showPanel: false });
}); });
@ -91,7 +93,7 @@ describe('initial state', () => {
const location = { search: qs.stringify(defaultLegacyParameters) }; const location = { search: qs.stringify(defaultLegacyParameters) };
const { const {
state: { layout, selectedPanel }, state: { layout, selectedPanel },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location, viewMode } });
// Nothing unexpected in layout // Nothing unexpected in layout
expect(layout).toEqual({}); expect(layout).toEqual({});
@ -111,7 +113,7 @@ describe('initial state', () => {
}; };
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ isFullscreen: true }); expect(layout).toEqual({ isFullscreen: true });
}); });
@ -127,7 +129,7 @@ describe('initial state', () => {
}; };
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ showNav: false, showPanel: false }); expect(layout).toEqual({ showNav: false, showPanel: false });
}); });
@ -142,7 +144,7 @@ describe('initial state', () => {
}; };
const { const {
state: { layout }, state: { layout },
} = initURL({ navigate, location }); } = initURL({ navigate, state: { location } });
expect(layout).toEqual({ panelPosition: 'right' }); expect(layout).toEqual({ panelPosition: 'right' });
}); });
@ -158,7 +160,7 @@ describe('queryParams', () => {
}, },
getState: () => state, getState: () => state,
}; };
const { api } = initURL({ location: { search: '' }, navigate: jest.fn(), store }); const { api } = initURL({ state: { location: { search: '' } }, navigate: jest.fn(), store });
api.setQueryParams({ foo: 'bar' }); api.setQueryParams({ foo: 'bar' });

View File

@ -1,2 +1,5 @@
declare module 'global'; declare module 'global';
declare module 'telejson'; declare module 'telejson';
// provided by the webpack define plugin
declare var DOCS_MODE: string | undefined;

View File

@ -7,6 +7,7 @@ import { DocsPageWrapper } from '../DocsPage';
export default { export default {
Component: PropRow, Component: PropRow,
title: 'Docs|PropRow', title: 'Docs|PropRow',
excludeStories: /.*Def$/,
decorators: [ decorators: [
getStory => ( getStory => (
<DocsPageWrapper> <DocsPageWrapper>
@ -18,7 +19,7 @@ export default {
], ],
}; };
const stringDef = { export const stringDef = {
name: 'someString', name: 'someString',
type: { name: 'string' }, type: { name: 'string' },
required: true, required: true,
@ -26,17 +27,17 @@ const stringDef = {
defaultValue: 'fixme', defaultValue: 'fixme',
}; };
const longNameDef = { export const longNameDef = {
...stringDef, ...stringDef,
name: 'reallyLongStringThatTakesUpSpace', name: 'reallyLongStringThatTakesUpSpace',
}; };
const longDescDef = { export const longDescDef = {
...stringDef, ...stringDef,
description: 'really long description that takes up a lot of space. sometimes this happens.', description: 'really long description that takes up a lot of space. sometimes this happens.',
}; };
const numberDef = { export const numberDef = {
name: 'someNumber', name: 'someNumber',
type: { name: 'number' }, type: { name: 'number' },
required: false, required: false,
@ -44,7 +45,7 @@ const numberDef = {
defaultValue: 0, defaultValue: 0,
}; };
const objectDef = { export const objectDef = {
name: 'someObject', name: 'someObject',
type: { name: 'objectOf', value: { name: 'number' } }, type: { name: 'objectOf', value: { name: 'number' } },
required: false, required: false,
@ -52,7 +53,7 @@ const objectDef = {
defaultValue: { value: '{ key: 1 }', computed: false }, defaultValue: { value: '{ key: 1 }', computed: false },
}; };
const arrayDef = { export const arrayDef = {
name: 'someOArray', name: 'someOArray',
type: { name: 'arrayOf', value: { name: 'number' } }, type: { name: 'arrayOf', value: { name: 'number' } },
required: false, required: false,
@ -60,7 +61,7 @@ const arrayDef = {
defaultValue: { value: '[1, 2, 3]', computed: false }, defaultValue: { value: '[1, 2, 3]', computed: false },
}; };
const complexDef = { export const complexDef = {
name: 'someComplex', name: 'someComplex',
type: { type: {
name: 'objectOf', name: 'objectOf',

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { PropsTable, PropsTableError } from './PropsTable'; import { PropsTable, PropsTableError } from './PropsTable';
import { DocsPageWrapper } from '../DocsPage'; import { DocsPageWrapper } from '../DocsPage';
import * as rowStories from './PropRow.stories'; import { stringDef, numberDef } from './PropRow.stories';
export default { export default {
Component: PropsTable, Component: PropsTable,
@ -13,6 +13,4 @@ export const error = () => <PropsTable error={PropsTableError.NO_COMPONENT} />;
export const empty = () => <PropsTable rows={[]} />; export const empty = () => <PropsTable rows={[]} />;
const { row: stringRow } = rowStories.string().props; export const normal = () => <PropsTable rows={[stringDef, numberDef]} />;
const { row: numberRow } = rowStories.number().props;
export const normal = () => <PropsTable rows={[stringRow, numberRow]} />;

View File

@ -21,6 +21,20 @@ const classes = {
ERROR: 'sb-show-errordisplay', ERROR: 'sb-show-errordisplay',
}; };
function matches(storyKey, arrayOrRegex) {
if (Array.isArray(arrayOrRegex)) {
return arrayOrRegex.includes(storyKey);
}
return storyKey.match(arrayOrRegex);
}
export function isExportStory(key, { includeStories, excludeStories }) {
return (
(!includeStories || matches(key, includeStories)) &&
(!excludeStories || !matches(key, excludeStories))
);
}
function showMain() { function showMain() {
document.body.classList.remove(classes.NOPREVIEW); document.body.classList.remove(classes.NOPREVIEW);
document.body.classList.remove(classes.ERROR); document.body.classList.remove(classes.ERROR);
@ -283,7 +297,7 @@ export default function start(render, { decorateStory } = {}) {
); );
} }
const { default: meta, ...examples } = fileExports; const { default: meta, ...exports } = fileExports;
const kindName = meta.title; const kindName = meta.title;
if (previousExports[filename]) { if (previousExports[filename]) {
@ -307,10 +321,12 @@ export default function start(render, { decorateStory } = {}) {
kind.addParameters(meta.parameters); kind.addParameters(meta.parameters);
} }
Object.keys(examples).forEach(key => { Object.keys(exports).forEach(key => {
const example = examples[key]; if (isExportStory(key, meta)) {
const { title = example.title || key, parameters } = example; const story = exports[key];
kind.add(title, example, parameters); const { title = story.title || key, parameters } = story;
kind.add(title, story, parameters);
}
}); });
previousExports[filename] = fileExports; previousExports[filename] = fileExports;

View File

@ -2,7 +2,7 @@
import { history, document, window } from 'global'; import { history, document, window } from 'global';
import Events from '@storybook/core-events'; import Events from '@storybook/core-events';
import start from './start'; import start, { isExportStory } from './start';
jest.mock('@storybook/client-logger'); jest.mock('@storybook/client-logger');
jest.mock('global', () => ({ jest.mock('global', () => ({
@ -142,3 +142,37 @@ describe('STORY_INIT', () => {
expect(store.setSelection).toHaveBeenCalledWith({ storyId: 'kind--story' }); expect(store.setSelection).toHaveBeenCalledWith({ storyId: 'kind--story' });
}); });
}); });
describe('story filters for module exports', () => {
it('should include all stories when there are no filters', () => {
expect(isExportStory('a', {})).toBeTruthy();
});
it('should filter stories by arrays', () => {
expect(isExportStory('a', { includeStories: ['a'] })).toBeTruthy();
expect(isExportStory('a', { includeStories: [] })).toBeFalsy();
expect(isExportStory('a', { includeStories: ['b'] })).toBeFalsy();
expect(isExportStory('a', { excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { excludeStories: [] })).toBeTruthy();
expect(isExportStory('a', { excludeStories: ['b'] })).toBeTruthy();
expect(isExportStory('a', { includeStories: ['a'], excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { includeStories: [], excludeStories: [] })).toBeFalsy();
expect(isExportStory('a', { includeStories: ['a'], excludeStories: ['b'] })).toBeTruthy();
});
it('should filter stories by regex', () => {
expect(isExportStory('a', { includeStories: /a/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /.*/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /b/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /a/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /.*/ })).toBeFalsy();
expect(isExportStory('a', { excludeStories: /b/ })).toBeTruthy();
expect(isExportStory('a', { includeStories: /a/, excludeStories: ['a'] })).toBeFalsy();
expect(isExportStory('a', { includeStories: /.*/, excludeStories: /.*/ })).toBeFalsy();
expect(isExportStory('a', { includeStories: /a/, excludeStories: /b/ })).toBeTruthy();
});
});

View File

@ -343,5 +343,6 @@ export async function buildDev({ packageJson, ...loadOptions }) {
...loadOptions, ...loadOptions,
packageJson, packageJson,
configDir: loadOptions.configDir || cliOptions.configDir || './.storybook', configDir: loadOptions.configDir || cliOptions.configDir || './.storybook',
docsMode: !!cliOptions.docs,
}); });
} }

View File

@ -123,6 +123,7 @@ async function buildManager(configType, outputDir, configDir, options) {
configDir, configDir,
corePresets: [require.resolve('./manager/manager-preset.js')], corePresets: [require.resolve('./manager/manager-preset.js')],
frameworkPresets: options.frameworkPresets, frameworkPresets: options.frameworkPresets,
docsMode: options.docsMode,
}); });
if (options.debugWebpack) { if (options.debugWebpack) {
@ -203,5 +204,6 @@ export function buildStatic({ packageJson, ...loadOptions }) {
packageJson, packageJson,
configDir: loadOptions.configDir || cliOptions.configDir || './.storybook', configDir: loadOptions.configDir || cliOptions.configDir || './.storybook',
outputDir: loadOptions.outputDir || cliOptions.outputDir || './storybook-static', outputDir: loadOptions.outputDir || cliOptions.outputDir || './storybook-static',
docsMode: !!cliOptions.docs,
}); });
} }

View File

@ -36,6 +36,7 @@ async function getCLI(packageJson) {
.option('--quiet', 'Suppress verbose build output') .option('--quiet', 'Suppress verbose build output')
.option('--no-dll', 'Do not use dll reference') .option('--no-dll', 'Do not use dll reference')
.option('--debug-webpack', 'Display final webpack configurations for debugging purposes') .option('--debug-webpack', 'Display final webpack configurations for debugging purposes')
.option('--docs', 'Build a documentation-only site using addon-docs')
.parse(process.argv); .parse(process.argv);
// Workaround the `-h` shorthand conflict. // Workaround the `-h` shorthand conflict.

View File

@ -16,6 +16,7 @@ function getCLI(packageJson) {
.option('--loglevel [level]', 'Control level of logging during build') .option('--loglevel [level]', 'Control level of logging during build')
.option('--no-dll', 'Do not use dll reference') .option('--no-dll', 'Do not use dll reference')
.option('--debug-webpack', 'Display final webpack configurations for debugging purposes') .option('--debug-webpack', 'Display final webpack configurations for debugging purposes')
.option('--docs', 'Build a documentation-only site using addon-docs')
.parse(process.argv); .parse(process.argv);
logger.setLevel(program.loglevel); logger.setLevel(program.loglevel);

View File

@ -19,7 +19,16 @@ const coreDirName = path.dirname(require.resolve('@storybook/core/package.json')
const context = path.join(coreDirName, '../../node_modules'); const context = path.join(coreDirName, '../../node_modules');
const cacheDir = findCacheDir({ name: 'storybook' }); const cacheDir = findCacheDir({ name: 'storybook' });
export default ({ configDir, configType, entries, dll, outputDir, cache, babelOptions }) => { export default ({
configDir,
configType,
docsMode,
entries,
dll,
outputDir,
cache,
babelOptions,
}) => {
const { raw, stringified } = loadEnv(); const { raw, stringified } = loadEnv();
const isProd = configType === 'PRODUCTION'; const isProd = configType === 'PRODUCTION';
@ -63,6 +72,7 @@ export default ({ configDir, configType, entries, dll, outputDir, cache, babelOp
new DefinePlugin({ new DefinePlugin({
'process.env': stringified, 'process.env': stringified,
NODE_ENV: JSON.stringify(process.env.NODE_ENV), NODE_ENV: JSON.stringify(process.env.NODE_ENV),
DOCS_MODE: docsMode, // global docs mode
}), }),
// See https://github.com/graphql/graphql-language-service/issues/111#issuecomment-306723400 // See https://github.com/graphql/graphql-language-service/issues/111#issuecomment-306723400
new ContextReplacementPlugin(/graphql-language-service-interface[/\\]dist/, /\.js$/), new ContextReplacementPlugin(/graphql-language-service-interface[/\\]dist/, /\.js$/),

View File

@ -486,11 +486,13 @@ class Layout extends Component {
{isDragging ? <HoverBlocker /> : null} {isDragging ? <HoverBlocker /> : null}
{children({ {children({
mainProps: { mainProps: {
viewMode,
animate: !isDragging, animate: !isDragging,
isFullscreen, isFullscreen,
position: getMainPosition({ bounds, resizerNav, isNavHidden, isFullscreen, margin }), position: getMainPosition({ bounds, resizerNav, isNavHidden, isFullscreen, margin }),
}, },
previewProps: { previewProps: {
viewMode,
animate: !isDragging, animate: !isDragging,
isFullscreen, isFullscreen,
isToolshown, isToolshown,
@ -506,6 +508,7 @@ class Layout extends Component {
}), }),
}, },
navProps: { navProps: {
viewMode,
animate: !isDragging, animate: !isDragging,
hidden: isNavHidden, hidden: isNavHidden,
position: { position: {
@ -516,6 +519,7 @@ class Layout extends Component {
}, },
}, },
panelProps: { panelProps: {
viewMode,
animate: !isDragging, animate: !isDragging,
align: options.panelPosition, align: options.panelPosition,
hidden: isPanelHidden, hidden: isPanelHidden,

View File

@ -15,6 +15,8 @@ ThemeProvider.displayName = 'ThemeProvider';
HelmetProvider.displayName = 'HelmetProvider'; HelmetProvider.displayName = 'HelmetProvider';
const Container = process.env.XSTORYBOOK_EXAMPLE_APP ? React.StrictMode : React.Fragment; const Container = process.env.XSTORYBOOK_EXAMPLE_APP ? React.StrictMode : React.Fragment;
// eslint-disable-next-line no-undef
const docsMode = !!DOCS_MODE; // webpack-injected
const Root = ({ provider }) => ( const Root = ({ provider }) => (
<Container key="container"> <Container key="container">
@ -22,7 +24,12 @@ const Root = ({ provider }) => (
<LocationProvider key="location.provider"> <LocationProvider key="location.provider">
<Location key="location.consumer"> <Location key="location.consumer">
{locationData => ( {locationData => (
<ManagerProvider key="manager" provider={provider} {...locationData}> <ManagerProvider
key="manager"
provider={provider}
{...locationData}
docsMode={docsMode}
>
{({ state }) => ( {({ state }) => (
<ThemeProvider key="theme.provider" theme={ensureTheme(state.theme)}> <ThemeProvider key="theme.provider" theme={ensureTheme(state.theme)}>
<App key="app" viewMode={state.viewMode} layout={state.layout} /> <App key="app" viewMode={state.viewMode} layout={state.layout} />