storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.08k stars 9.25k forks source link

Accessing @decorator tags in component JSDoc comments #18162

Open rikkit opened 2 years ago

rikkit commented 2 years ago

Is your feature request related to a problem? Please describe

tl;dr; I was using SB 6.0 and recently upgraded to SB 6.4. I used to have something working, and now __docgenInfo doesn't contain the data I need. Looking for a pointer to how to get it back!


I'm maintaining a design system. We're using Storybook for documentation reasons. One of the ideas we had was to extend the feature of addon docs that shows JSDoc comments on docs pages. If there is e.g. an @status deprecated tag on the component we'd show a red deprecated label on the component's doc page. There was also @status stable, @status development, @status review.

This feature was working in SB6.0 with the following decorator. Essentially, look at __docgenInfo.description, do a regex match, and set __docgenInfo.status if one is present.

// statusDocsDecorator.tsx
import React from "react";
import { useContext } from "react";
import { addParameters } from '@storybook/react';
import { DocsContext, DocsPage } from '@storybook/addon-docs';
import { StatusBadge, ComponentStatus } from "../src/atoms/StatusBadge/StatusBadge";

/**
 * Decorator to display a component's status in its docspage.
 */
export default addParameters({
  docs: {
    page: () => {
      const context = useContext(DocsContext);

      // This is a bit hacky; ideally parsing the status could be done when the babel loader
      // initally generates __docgenInfo
      let status = context.parameters.component.__docgenInfo.status as ComponentStatus;
      if (!context.parameters.component.__docgenInfo.status) {
        const description: string = context.parameters.component.__docgenInfo.description ?? "";
        const statusMatch = description.match(/@status\s+(.*)/);

        status = statusMatch?.[1] as ComponentStatus;
        if (status) {
          context.parameters.component.__docgenInfo.description = description.replace(statusMatch?.[0]!, `\nComponent status: ${status}`)
          context.parameters.component.__docgenInfo.status = status;
        }
      }

      return (
        <>
          {status && <StatusBadge status={status} />}
          <DocsPage />
        </>
      );
    },
  },
});

An example of a story:

/**
 * Component for [Bulma's Container](https://bulma.io/documentation/layout/container/).
 * @status deprecated
 */
export const Container: React.FC<Props> = ({ children, className, Element = "div" }) => {
  return (
    <Element className={classNames("container", "tl-container", className)}>
      {children}
    </Element>
  )
}

I upgraded to SB6.4 and now it appears that __docgenInfo.description now never contains any @ JSDoc tags.

I have looked through the sources of addon-docs, react-docgen, babel-plugin-react-docgen but I can't track down where the comment is actually parsed from the story source...

Describe the solution you'd like

A pointer to where/ when this was changed, or a way to access the raw JSDoc comment again.

Describe alternatives you've considered To not do this ;)

Are you able to assist to bring the feature to reality? Sure if I know where to look....

Additional context Thanks!

MichaelArestad commented 2 years ago

Can you test the minor releases from 6.0 to 6.4 to help us identify where the regression happened?

shilman commented 2 years ago

This most likely broke in 6.4

@tmeasday any suggestions?

tmeasday commented 2 years ago

@shilman I'm not aware of anything that's changed WRT to docgen? Did we bump versions? The only code I personally have looked at that deals with docgen was using the output of it; I didn't actually change the __docgenInfo annotations themselves.

unicornware commented 2 years ago

i'm facing a somewhat similar issue in that @see tags do not show up on my docs pages. i was hoping to access jsdoc tags to finally implement extractComponentDescription myself (i've had this issue for a while now 😅).

after doing some digging, i think part of the issue here is that __docgenInfo is not the entire ComponentDoc object. it only includes description, displayName, and props:

Screen Shot 2022-05-17 at 14 45 59

not sure just how new of a feature this is, but react-docgen-typescript actually includes jsdoc tags as a separate property:

[
  {
    tags: {
      see: 'https://developer.mozilla.org/docs/Web/HTML/Element/a\n' +
        'https://developer.mozilla.org/docs/Web/API/HTMLAnchorElement\n' +
        'https://www.w3.org/TR/wai-aria-practices-1.1/#link'
    },
    description: 'Renders an HTML `<a>` element.\n' +
      '\n' +
      '`Anchor` is a widget that provides an interactive reference to a resource.\n' +
      'The target resource, `href`, can be either external or local (either outside\n' +
      'or within the current page or application).',
    displayName: 'Anchor',
    methods: [],
    props: {
      slot: [Object],
      style: [Object],
      title: [Object],
      type: [Object],
      name: [Object],
      ref: [Object],
      key: [Object],
      download: [Object],
      href: [Object],
      hrefLang: [Object],
      media: [Object],
      ping: [Object],
      rel: [Object],
      target: [Object],
      referrerPolicy: [Object],
      defaultChecked: [Object],
      defaultValue: [Object],
      suppressContentEditableWarning: [Object],
      suppressHydrationWarning: [Object],
      accessKey: [Object],
      className: [Object],
      contentEditable: [Object],
      contextMenu: [Object],
      dir: [Object],
      draggable: [Object],
      hidden: [Object],
      id: [Object],
      lang: [Object],
      placeholder: [Object],
      spellCheck: [Object],
      tabIndex: [Object],
      translate: [Object],
      radioGroup: [Object],
      role: [Object],
      about: [Object],
      datatype: [Object],
      inlist: [Object],
      prefix: [Object],
      property: [Object],
      resource: [Object],
      typeof: [Object],
      vocab: [Object],
      autoCapitalize: [Object],
      autoCorrect: [Object],
      autoSave: [Object],
      color: [Object],
      results: [Object],
      security: [Object],
      unselectable: [Object],
      inputMode: [Object],
      is: [Object],
      children: [Object],
      dangerouslySetInnerHTML: [Object],
      disabled: [Object],
      theme: [Object],
      as: [Object],
      forwardedAs: [Object]
    },
    expression: SymbolObject {
      flags: 524288,
      escapedName: 'StyledComponent',
      declarations: [Array],
      parent: [SymbolObject],
      isReferenced: 788968,
      id: 446
    }
  }
]

rikkit commented 2 years ago

The change to move JSDoc tags into the tags property must be the issue - that didn't used to be the case, the tags were just in the string.

Where does __docgenInfo get created? When I looked I wasn't able to track down where this actually came from...

unicornware commented 2 years ago

@rikkit howdy! i finally had a chance to dig in and do some more debugging, so i'm back with a few updates! hopefully they'll still be of some help to you.

first, react-docgen-typescript supports an option titled shouldIncludePropTagMap. not sure when it was introduced, but when set to true, ComponentDoc objects include a tags property (i think i had this set when i originally posted, otherwise i don't think i would've noticed the change in the first place). to pass the option via storybook, set shouldIncludePropTagMap under typescript.reactDocgenTypescriptOptions.

to answer your question - for both @storybook/react-docgen-typescript-plugin and @joshwooding/vite-plugin-react-docgen-typescript, i'm 98.999% sure __docgenInfo is created via setComponentDocGen. it's somewhat hidden given the implementations, but if you search "__docgenInfo" while on either page, you should be able to track it down. if that doesn't tickle your fancy, here's a simplified version of the same core logic (excuse the shameless code plug 😅):

import MagicString from 'magic-string'
import micromatch from 'micromatch'
import path from 'node:path'
import {
  withCustomConfig,
  type ComponentDoc,
  type FileParser
} from 'react-docgen-typescript'
import type { TransformResult } from 'rollup'
import dedent from 'ts-dedent'
import type { Plugin } from 'vite'
import { PLUGIN_NAME } from './constants'
import type Options from './options.interface'

/**
 * Creates a `react-docgen-typescript` plugin.
 *
 * @see https://github.com/styleguidist/react-docgen-typescript
 * @see https://vitejs.dev/guide/api-plugin.html
 *
 * @param {Options} [options] - Plugin options
 * @return {Plugin} Vite `react-docgen-typescript` plugin
 */
const docgen = ({
  apply,
  componentNameResolver,
  enforce = 'pre',
  customComponentTypes = [],
  exclude = ['**.stories.tsx'],
  handler = doc => doc,
  include = ['**.tsx'],
  name = doc => doc.displayName,
  propFilter = prop => !prop.parent?.fileName.includes('node_modules'),
  savePropValueAsString = false,
  shouldExtractLiteralValuesFromEnum = false,
  shouldExtractValuesFromUnion = false,
  shouldIncludeExpression = false,
  shouldIncludePropTagMap = true,
  shouldRemoveUndefinedFromOptional = true,
  skipChildrenPropWithoutDoc = true,
  tsconfigPath = path.resolve('tsconfig.json')
}: Options = {}): Plugin => {
  /**
   * Component docgen info parser.
   *
   * @see https://github.com/styleguidist/react-docgen-typescript#usage
   *
   * @const {FileParser} parser
   */
  const parser: FileParser = withCustomConfig(tsconfigPath, {
    componentNameResolver,
    customComponentTypes,
    propFilter,
    savePropValueAsString,
    shouldExtractLiteralValuesFromEnum,
    shouldExtractValuesFromUnion,
    shouldIncludeExpression,
    shouldIncludePropTagMap,
    shouldRemoveUndefinedFromOptional,
    skipChildrenPropWithoutDoc
  })

  return {
    apply,
    enforce,
    name: PLUGIN_NAME,
    /**
     * Parses component docgen info from `id`.
     *
     * The final transform result will include a new source map and updated
     * version of `code` that includes a code block to attach a `__docgenInfo`
     * property to the component exported from `id`.
     *
     * If `id` isn't explicity included via {@link include}, or is explicity
     * excluded from transformation via {@link exclude}, `undefined` will be
     * returned instead.
     *
     * @param {string} code - Source code
     * @param {string} id - Module id
     * @return {Exclude<TransformResult, string>} Transformation result
     */
    transform(code: string, id: string): Exclude<TransformResult, string> {
      // do nothing if file isn't explicity omitted
      if (!micromatch.isMatch(id, include)) return

      // do nothing if file is explicity omitted
      if (micromatch.isMatch(id, exclude)) return

      try {
        /**
         * Component docgen info.
         *
         * @see https://github.com/styleguidist/react-docgen-typescript/blob/v2.2.2/src/parser.ts#L16
         *
         * @var {ComponentDoc?} doc
         */
        let doc: ComponentDoc | undefined = parser.parse(id).pop()

        // bail if missing component docgen info
        if (!doc) return null

        /**
         * {@link code} as `MagicString`.
         *
         * @see https://github.com/Rich-Harris/magic-string
         *
         * @const {MagicString} src
         */
        const src: MagicString = new MagicString(code)

        // apply additional transformations to component docgen info
        doc = handler(doc, id, code)

        /**
         * Code block containing logic to attach a `__docgenInfo` property to
         * the component in found in {@link code}.
         *
         * @const {string} docgenblock
         */
        const docgenblock: string = dedent`
          try {
            ${name(doc, id, code)}.__docgenInfo=${JSON.stringify(doc)};
          } catch (e) {
            console.error('[${PLUGIN_NAME}]' + ' ' + e.message, e)
          }
        `

        // add __docgenInfo code block to source code
        src.append(docgenblock)

        return {
          code: src.toString(),
          map: src.generateMap({ hires: true, source: id })
        }
      } catch (e: unknown) {
        return void console.error(e instanceof Error ? e.message : e)
      }
    }
  }
}

export default docgen

side note: i'm using storybook with vite now, so that's why i included the info about the vite plugin. i'm using a custom vite plugin rather than @joshwooding/vite-plugin-react-docgen-typescript because i was debugging an issue with my docs not reflecting changes during development.

nicodeclercq commented 1 year ago

Is there any news on this ? The typescript.reactDocgenTypescriptOptions.shouldIncludePropTagMap : true fix doesn't seem to be working on my side...