UNPKG

orphic-cypress

Version:

Set of utilities and typescript transformers to cover storybook stories with cypress component tests

198 lines (182 loc) 5.36 kB
import * as React from "react"; /** * Check if the first prop of a */ const isRawMd = (childProps: { mdxType?: string; children?: { props?: { className?: string } }; }): boolean => childProps.mdxType === "pre" && childProps.children?.props?.className === "language-md"; /** Rendered child, with props */ export type RenderedChild = any; /** mdx component as it exists after importing via `import someMdx from "./some.mdx"` */ export type Mdx = (props: unknown) => RenderedChild; /** Object gathered for each header */ export type ParsedMdx = { /** Full content of this segment of markdown including the header */ full: RenderedChild[]; /** Content of this segment of markdown excluding header */ body: RenderedChild[]; /** Raw string extracted from 'md' code blocks */ md: string; }; /** Function returned for each header, with properties assigned for more specific use cases */ export type MdxSegment = { /** Function which is useful passed to parameters.docs.page directly */ (): RenderedChild[]; } & ParsedMdx; /** Header in kebab case as key to object of markdown segments */ export type HeaderKeyedMdxSegment = { [id: string]: MdxSegment }; /** * quick and dirty fifo * @private */ export class Fifo<T, U> { limit: number; _cache: Map<T, U>; // TODO: babel was getting upset with `private` keyword constructor(limit = 50, _cache = new Map<T, U>()) { this.limit = limit; this._cache = _cache; } get(key: T) { return this._cache.get(key); } set(key: T, val: U) { if (this._cache.size === this.limit) { this._cache.delete(this._cache.keys().next().value); } this._cache.set(key, val); return val; } } const cache = new Fifo<Mdx, HeaderKeyedMdxSegment>(); /** * simple kebab-case converter for space separated text, * returns undefined if str is undefined or null * @private */ export const safeKebabCase = (str?: string | null) => typeof str === "string" ? str.toLowerCase().replace(/ /g, "-") : null; /** * Split up an MDX files into headers for easy use in multiple parts of documentation * or in multiple files, with some added perks. * * Currently, this breaks on any header such that a file like * ~~~md * # First Component * * Something * * ## Second Component * * ```md * # Second header description * This second component does stuff * ``` * ~~~ * becomes essentially * ```ts * { * "first-component": { * full: [<h1>First Component</h1>,<p>Something</p>], * body: [<p>Something</p>], * md: "", * }, * "second-component": { * full: [<h1>Second Component</h1>,<p>Other</p>], * body: [<code>....</code>], * md: "# Second header description\nThis second component does stuff", * }, * } * ``` * Although actually they'll be functions at those locations that also have those properties, * but is `() => full` at invocation. Note how it picks up md code blocks as raw text, suitable * for story descriptions. * * * Then you can use it like * ```ts * import mdx from "./some.mdx"; * const mdxObject = segmentMdx(mdx); * // define FirstComponent... * FirstComponent.parameters = { * docs: { * page: mdxObject['first-component'], * } * }; * // define SecondComponent... * SecondComponent.parameters = { * docs: { * story: { * description: mdxObject['second-component'].md, * } * } * }; * ``` * * And if you needed to combine them you could do something like * ```ts * docs: { * page: () => [ * ...mdxObject["first-component"].full, * ...mdxObject["second-component"].full, * ] * } * ``` * * Or, in an mdx file like so (real example): * ```md * import { Meta } from "@storybook/addon-docs"; * import readme from "../../README.md"; * import { segmentMdx } from "orphic-cypress"; * * <Meta title="MockRequests/Overview" /> * * <>{segmentMdx(readme)["intercepting-api-requests"].full}</> * * <-- more markdown --> * # Further afield * ``` * * Uses a dead simple FIFO cache of size 50 just to avoid thinking about memory consumption issues. */ export const segmentMdx = ( mdx: Mdx, /** force skipping the cache */ force?: boolean ): HeaderKeyedMdxSegment => { const fromCache = !force && cache.get(mdx); if (fromCache) return fromCache; if (typeof mdx !== "function") return cache.set(mdx, {}); const rendered = mdx({}); let currentId = "file"; const collection: { [id: string]: ParsedMdx } = { file: { full: [], body: [], md: "" }, }; React.Children.forEach(rendered.props.children, (child) => { const childrenOfChild = child.props.children; if (/^h\d$/.test(child.props.mdxType)) { // not sure why exactly the id is sometimes already present currentId = child.props.id || safeKebabCase(childrenOfChild) || "unknown"; collection[currentId] = { full: [child], body: [], md: "" }; } else if (collection[currentId]) { collection[currentId].full.push(child); collection[currentId].body.push(child); if (isRawMd(child.props)) { const rawMd = childrenOfChild.props.children; collection[currentId].md += rawMd; } } }); return cache.set( mdx, Object.fromEntries( Object.entries(collection).map(([k, v]): [string, MdxSegment] => [ k, Object.assign(() => v.full, v), ]) ) ); };