@storybook/react
Version:
Storybook for React: Develop React Component in isolation with Hot Reloading.
177 lines (141 loc) • 7.02 kB
JavaScript
const _excluded = ["mdxType", "originalType", "children"];
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
import React, { createElement } from 'react';
import reactElementToJSXString from 'react-element-to-jsx-string';
import dedent from 'ts-dedent';
import deprecate from 'util-deprecate';
import { addons, useEffect } from '@storybook/addons';
import { SourceType, SNIPPET_RENDERED, getDocgenSection } from '@storybook/docs-tools';
import { logger } from '@storybook/client-logger';
import { isMemo, isForwardRef } from './lib';
/** Run the user supplied onBeforeRender function if it exists */
const applyBeforeRender = (domString, options) => {
if (typeof options.onBeforeRender !== 'function') {
return domString;
}
const deprecatedOnBeforeRender = deprecate(options.onBeforeRender, dedent`
StoryFn.parameters.jsx.onBeforeRender was deprecated.
Prefer StoryFn.parameters.jsx.transformSource instead.
See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-onbeforerender for details.
`);
return deprecatedOnBeforeRender(domString);
};
/** Run the user supplied transformSource function if it exists */
const applyTransformSource = (domString, options, context) => {
if (typeof options.transformSource !== 'function') {
return domString;
}
return options.transformSource(domString, context);
};
/** Apply the users parameters and render the jsx for a story */
export const renderJsx = (code, options) => {
if (typeof code === 'undefined') {
logger.warn('Too many skip or undefined component');
return null;
}
let renderedJSX = code;
const Type = renderedJSX.type;
for (let i = 0; i < options.skip; i += 1) {
if (typeof renderedJSX === 'undefined') {
logger.warn('Cannot skip undefined element');
return null;
}
if (React.Children.count(renderedJSX) > 1) {
logger.warn('Trying to skip an array of elements');
return null;
}
if (typeof renderedJSX.props.children === 'undefined') {
logger.warn('Not enough children to skip elements.');
if (typeof renderedJSX.type === 'function' && renderedJSX.type.name === '') {
renderedJSX = /*#__PURE__*/React.createElement(Type, renderedJSX.props);
}
} else if (typeof renderedJSX.props.children === 'function') {
renderedJSX = renderedJSX.props.children();
} else {
renderedJSX = renderedJSX.props.children;
}
}
const displayNameDefaults = typeof options.displayName === 'string' ? {
showFunctions: true,
displayName: () => options.displayName
} : {
// To get exotic component names resolving properly
displayName: el => el.type.displayName || getDocgenSection(el.type, 'displayName') || (el.type.name !== '_default' ? el.type.name : null) || (typeof el.type === 'function' ? 'No Display Name' : null) || (isForwardRef(el.type) ? el.type.render.name : null) || (isMemo(el.type) ? el.type.type.name : null) || el.type
};
const filterDefaults = {
filterProps: (value, key) => value !== undefined
};
const opts = Object.assign({}, displayNameDefaults, filterDefaults, options);
const result = React.Children.map(code, c => {
// @ts-ignore FIXME: workaround react-element-to-jsx-string
const child = typeof c === 'number' ? c.toString() : c;
let string = applyBeforeRender(reactElementToJSXString(child, opts), options);
if (string.indexOf('"') > -1) {
const matches = string.match(/\S+=\\"([^"]*)\\"/g);
if (matches) {
matches.forEach(match => {
string = string.replace(match, match.replace(/"/g, "'"));
});
}
}
return string;
}).join('\n');
return result.replace(/function\s+noRefCheck\(\)\s+\{\}/, '() => {}');
};
const defaultOpts = {
skip: 0,
showFunctions: false,
enableBeautify: true,
showDefaultProps: false
};
export const skipJsxRender = context => {
var _context$parameters$d;
const sourceParams = context === null || context === void 0 ? void 0 : (_context$parameters$d = context.parameters.docs) === null || _context$parameters$d === void 0 ? void 0 : _context$parameters$d.source;
const isArgsStory = context === null || context === void 0 ? void 0 : context.parameters.__isArgsStory; // always render if the user forces it
if ((sourceParams === null || sourceParams === void 0 ? void 0 : sourceParams.type) === SourceType.DYNAMIC) {
return false;
} // never render if the user is forcing the block to render code, or
// if the user provides code, or if it's not an args story.
return !isArgsStory || (sourceParams === null || sourceParams === void 0 ? void 0 : sourceParams.code) || (sourceParams === null || sourceParams === void 0 ? void 0 : sourceParams.type) === SourceType.CODE;
};
const isMdx = node => {
var _node$type, _node$props;
return ((_node$type = node.type) === null || _node$type === void 0 ? void 0 : _node$type.displayName) === 'MDXCreateElement' && !!((_node$props = node.props) !== null && _node$props !== void 0 && _node$props.mdxType);
};
const mdxToJsx = node => {
if (!isMdx(node)) return node;
const _node$props2 = node.props,
{
originalType,
children
} = _node$props2,
rest = _objectWithoutPropertiesLoose(_node$props2, _excluded);
let jsxChildren = [];
if (children) {
const array = Array.isArray(children) ? children : [children];
jsxChildren = array.map(mdxToJsx);
}
return /*#__PURE__*/createElement(originalType, rest, ...jsxChildren);
};
export const jsxDecorator = (storyFn, context) => {
var _context$parameters$d2, _context$parameters$d3;
const channel = addons.getChannel();
const skip = skipJsxRender(context);
const story = storyFn();
let jsx = '';
useEffect(() => {
if (!skip) channel.emit(SNIPPET_RENDERED, (context || {}).id, jsx);
}); // We only need to render JSX if the source block is actually going to
// consume it. Otherwise it's just slowing us down.
if (skip) {
return story;
}
const options = Object.assign({}, defaultOpts, (context === null || context === void 0 ? void 0 : context.parameters.jsx) || {}); // Exclude decorators from source code snippet by default
const storyJsx = context !== null && context !== void 0 && (_context$parameters$d2 = context.parameters.docs) !== null && _context$parameters$d2 !== void 0 && (_context$parameters$d3 = _context$parameters$d2.source) !== null && _context$parameters$d3 !== void 0 && _context$parameters$d3.excludeDecorators ? context.originalStoryFn(context.args, context) : story;
const sourceJsx = mdxToJsx(storyJsx);
const rendered = renderJsx(sourceJsx, options);
if (rendered) {
jsx = applyTransformSource(rendered, options, context);
}
return story;
};