break-stuff / wc-storybook-helpers

Helpers designed to make integrating Web Components with Storybook easier.
MIT License
47 stars 9 forks source link

custom jsdoc storybook blocks #43

Open pascalvos opened 6 months ago

pascalvos commented 6 months ago

like discussed on discord :)

the idea is to be able to use custom jsdoc annotations to document things in storybook based on this work https://github.com/shoelace-style/shoelace/blob/dafb35c6e210193a9ca31efddc3429ba2bb66be3/custom-elements-manifest.config.js

for example, we can use @since annotation to get a version this could be in turn used to be displayed in storybook. i did some hacking around with storyblocks and i got something based on this plugin

annotation example based on shoelace example

/**
 * A button is used to trigger an action. For more information about a button, click [here](https://unify.nedap.healthcare/42a5b6c3c/p/17c689-button/b/05c6fc)
 *
 * @status stable
 * @since 2.0.0
 *
 * @dependency uc-icon
 * @dependency uc-badge
 *
 **/

the cem plugin

import {parse} from 'comment-parser';

function noDash(string) {
  return string.replace(/^\s?-/, '').trim();
}

export function customTags() {
  return {
    name: 'custom-tags',
    analyzePhase({ts, node, moduleDoc}) {
      switch (node.kind) {
        case ts.SyntaxKind.ClassDeclaration: {
          const className = node.name.getText();
          const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
          const customTags = ['aria-rules', 'deprecated-attribute','dependency', 'status', 'since'];
          let customComments = '/**';

          node.jsDoc?.forEach(jsDoc => {
            jsDoc?.tags?.forEach(tag => {
              const tagName = tag.tagName.getText();

              if (customTags.includes(tagName)) {
                customComments += `\n * @${tagName} ${tag.comment}`;
              }
            });
          });

          // This is what allows us to map JSDOC comments to ReactWrappers.
          classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n');

          const parsed = parse(`${customComments}\n */`);
          parsed[0].tags?.forEach(t => {
            switch (t.tag) {
              // custom rule for aria
              case 'aria-rules':
                if (!Array.isArray(classDoc['aria-rules'])) {
                  classDoc['aria-rules'] = [];
                }
                classDoc['aria-rules'].push({
                  name: t.name,
                  description: noDash(t.description),
                });
                break;

              // custom deprecated attribute rule
              case 'deprecated-attribute':
                if (!Array.isArray(classDoc['deprecated-attribute'])) {
                  classDoc['deprecated-attribute'] = [];
                }
                classDoc['deprecated-attribute'].push({
                  name: t.name,
                  description: noDash(t.description),
                });
                break;

              // custom deprecated element rule
              case 'deprecated':
                if (!Array.isArray(classDoc['deprecated'])) {
                  classDoc['deprecated'] = [];
                }
                classDoc['deprecated'].push({
                  description: noDash(t.description),
                });
                break;

              // Dependencies
              case 'dependency':
                if (!Array.isArray(classDoc['dependencies'])) {
                  classDoc['dependencies'] = [];
                }
                classDoc['dependencies'].push(t.name);
                break;

              // Value-only metadata tags
              case 'since':
              case 'status':
                classDoc[t.tag] = t.name;
                break;

              // All other tags
              default:
                if (!Array.isArray(classDoc[t.tag])) {
                  classDoc[t.tag] = [];
                }

                classDoc[t.tag].push({
                  name: t.name,
                  description: t.description,
                  type: t.type || undefined
                });
            }
          });
        }
      }
    }
  }
}

preview.ts

import { setCustomElementsManifest } from "@storybook/web-components";
import { setWcStorybookHelpersConfig } from "wc-storybook-helpers";
import customElements from "custom-elements.json";
import DocumentationTemplate from "./DocumentationTemplate.mdx";

setWcStorybookHelpersConfig({ typeRef: "expandedType", hideArgRef: true });
setCustomElementsManifest(customElements);

const preview: Preview = {
    // ... more settings and things
    docs: {
      page: DocumentationTemplate,
    },
}
export default preview;

DocumentationTemplate.mdx

import { Meta, Title, Subtitle, Primary, Controls, Stories, Description } from '@storybook/blocks';
import MyCustomBlock from './MyCustomBlock';

{/*
  * 👇 The isTemplate property is required to tell Storybook that this is a template
  * See https://storybook.js.org/docs/api/doc-block-meta
  * to learn how to use
*/}

<Meta isTemplate />

<Title />

<Subtitle/>

<Description />

<MyCustomBlock />

# Default implementation

<Primary />

## Inputs

The component accepts the following inputs (props):

<Controls />

---

## Additional variations

Listed below are additional variations of the component.

<Stories />

MyCustomBlock.jsx

import React, { useContext } from "react";
import { DocsContext } from "@storybook/addon-docs/blocks";

import customElements from "@nedap/unify-components/custom-elements.json";
import { getComponentByTagName } from "wc-storybook-helpers/dist/cem-utilities.js";

const MyCustomBlock = ({ children, ...props }) => {
  const context = useContext(DocsContext);
  const tag = context.storyById().component;
  const component = getComponentByTagName(tag, customElements);
  const { since, dependencies, status } = component;

  return (
    <div {...props}>
      {since && <p>Since: {since}</p>}
      {dependencies && (
        <p>
          Dependencies:
          <ul>
            {dependencies.map((dep) => (
              <li>{dep}</li>
            ))}
          </ul>
        </p>
      )}
      {status && <p>Status: {status}</p>}
      {/* Render your custom content or children */}
      {children}
    </div>
  );
};

export default MyCustomBlock;
pascalvos commented 6 months ago

Screenshot 2024-02-09 at 15 17 22 this is how it pops up on the screen

break-stuff commented 6 months ago

The first piece of this has been created:

https://www.npmjs.com/package/cem-plugin-custom-jsdoc-tags

pascalvos commented 6 months ago

sweet 👍 I'll see if I can get it integrated over the next few weeks