orphic-cypress
Version:
Set of utilities and typescript transformers to cover storybook stories with cypress component tests
207 lines • 8.75 kB
JavaScript
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
;