UNPKG

orphic-cypress

Version:

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

207 lines 8.75 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.transformSource = exports.removeSkips = void 0; const regex = { /** Any comment syntax (really, with any number of stars) with story-code at start */ init: "(//|/\\*+)(\\s+)?story-code(\\s+?)", /** * Include from start of file to after default block, * can be defined on @end line or 1 after end */ includeStart: "@include-start", /** Include the default block, can be defined on @end line or 1 after end */ includeDefault: "@include-default", }; /** * Create named capture groups for @skip-end etc. A match but no groups * means its a normal `@skip` line with no suffix */ const skipRegex = (() => { const skipParts = ["end", "start", "next"]; const skipJoin = skipParts.map((name) => `(?<${name}>-${name})`).join("|"); return new RegExp(`${regex.init}@skip(${skipJoin})?`, "gm"); })(); /** * Remove lines with @skip, after @skip-next or between @skip-start and * @skip-end, all starting with `// story-code ` * * @private */ const removeSkips = (codeLines) => { var _a; const skip = { next: false, block: false }; const lines = []; for (const line of codeLines) { const groups = (_a = line.matchAll(skipRegex).next().value) === null || _a === void 0 ? void 0 : _a.groups; if (skip.block) { if (groups === null || groups === void 0 ? void 0 : groups.end) skip.block = false; } else if (groups === null || groups === void 0 ? void 0 : groups.start) { skip.block = true; } else if (groups === null || groups === void 0 ? void 0 : groups.next) { skip.next = true; } else if (skip.next) { skip.next = false; } else if (groups === undefined) { lines.push(line); } } return lines; }; exports.removeSkips = removeSkips; const getDataFromStoryObject = (name, allLines) => { const componentName = name.replace(/ /g, ""); // Naive attempt to fix object syntax source const startIndex = allLines.findIndex((line) => new RegExp(`export const ${componentName}`).test(line)); const endIndex = allLines .slice(startIndex) .findIndex((line) => /^};/.test(line)); return [ componentName, { startLoc: { col: 0, line: startIndex }, endLoc: { col: 0, line: endIndex + startIndex + 1 }, }, ]; }; /** Some super simple validation on the location lines */ const validateLocation = (allLines, startLine, endLine) => { if (allLines.length < startLine) { throw new Error(`Start line of ${startLine} exceeds file length of ${allLines.length}`); } if (allLines.length < endLine) { throw new Error(`End line of ${endLine} exceeds file length of ${allLines.length}`); } }; /** check the comment line and end line for a given directive */ const checkForDirective = (linesFromStart, endLineComment, endLoc) => (re) => new RegExp(regex[re]).test(linesFromStart[endLineComment]) || new RegExp(`${regex.init}${regex[re]}`).test(linesFromStart[endLoc]); /** * Add comment directives that will enable transforming the story source code * into the code snippet for the story. * * Relies on storysource addon * * Notable issues: * * if a story is all object syntax, then it won't have * a storySource at all. That's likely a limitation of source-loader. * * If a story name is very long, story-loader's handling can get weird * * TODO: some notable naive approaches here. Could parse AST to get * the default export, to automatically include assignments to stories, or * to better parse story objects. * * TODO: Some of this might be suitable contribution to storysource, but * the goals there often different, e.g. to show how to use a component * whereas here we're showing how to build stories. * * TODO: ideas include-render, include-template, include-region * * All comments start with `// story-code` or with `/*` or `/**` style * single line comments. So `// story-code @end SomeComponent @include-default` * for example. * * ## Available commands: * * `@end`: end the previous story's code block. Note, this is works across * stories such that any story which does not specify its end which begins * before this use will end at this point. Use named end's if you need specificity * ```ts * const SomeStory: ComponentStory<typeof Comp> = (args) => <Comp {...args} />; * SomeStory.args = { prop: 1 }; * // story-code @end * ``` * * `@end SomeComponent`: same as above, but only mark the end for the given component * ```ts * const SomeStory: ComponentStory<typeof Comp> = (args) => <Comp {...args} />; * const OtherStory: ComponentStory<typeof Comp> = (args) => <Comp {...args} />; * OtherStory.args = { prop: 1 }; * // story-code @end OtherStory * ``` * * `@include-default`: include the default code export. Can occur in an `@end` line * or on the line following a natural or designated end. * ```ts * const SomeStory: ComponentStory<typeof Comp> = (args) => <Comp {...args} />; * SomeStory.args = { prop: 1 }; * // story-code @end @include-default * ``` * * `@include-start`: include from the top of the file through to the default code export. * Can occur in an `@end` line or on the line following a natural or designated end. * ```ts * const SomeStory: ComponentStory<typeof Comp> = (args) => <Comp {...args} />; * // story-code @include-start * ``` * * `@skip`: Skip the current line * ```ts * const somethingToIgnore = 1; // story-code @skip * ``` * * `@skip-next`: Skip the next line * ```ts * // story-code @skip-next * const somethingToIgnore = 1; * ``` * * `@skip-start` and `@skip-end`: Skip a block of text, e.g. * ```ts * // story-code @skip-start * const hideThis = 1; * const andThis = 2; * // story-code @skip-end * ``` * There's nothing enforcing that you have to have a @skip-end if you have a @skip-start */ const transformSource = (opts = {}) => /** Inner function which can be assigned to docs.transformSource */ (snippet, storyContext) => { var _a; try { const { parameters: { storySource: { source, locationsMap }, }, originalStoryFn, id, name, } = storyContext; let componentName = originalStoryFn.name; const locationKey = id.split("--")[1]; let location = locationsMap[locationKey]; const allLines = source.split("\n"); if (!location && opts.includeObjects) { [componentName, location] = getDataFromStoryObject(name, allLines); } const { startLoc: { line: startLine }, endLoc: { line: endLine }, } = location; validateLocation(allLines, startLine, endLine); const linesFromStart = allLines.slice(startLine - 1); const endLineComment = linesFromStart.findIndex((line) => new RegExp(`${regex.init}@end(\\s+)?($|@|${componentName})`).test(line)); const endLoc = endLineComment > 0 ? endLineComment : endLine - startLine + 1; const [includeDefault, includeStart] = ["includeDefault", "includeStart"].map(checkForDirective(linesFromStart, endLineComment, endLoc)); let defaultLines = []; if (includeDefault || includeStart) { // This is pretty naive const defaultStartIndex = allLines.findIndex((line) => /export default {/.test(line)); if (defaultStartIndex !== -1) { const linesFromDefaultStart = includeStart ? allLines : allLines.slice(defaultStartIndex); if ((_a = allLines[defaultStartIndex]) === null || _a === void 0 ? void 0 : _a.includes("};")) { defaultLines = [...linesFromDefaultStart.slice(0, 1), ""]; } else { const endDefaultLine = linesFromDefaultStart.findIndex((line) => /^};$/.test(line)); if (endDefaultLine > 0) { defaultLines = linesFromDefaultStart.slice(0, endDefaultLine + 1); if (defaultLines.at(-1) !== "") { defaultLines = [...defaultLines, ""]; } } } } } return (0, exports.removeSkips)([ ...defaultLines, ...linesFromStart.slice(0, endLoc), ]).join("\n"); } catch (e) { console.warn("Something went wrong while getting the story source for code snippet", e); return snippet; } }; exports.transformSource = transformSource; //# sourceMappingURL=story-code.js.map