Автоматизация документации React с помощью React-Docgen ⚛⚙️📚

В моем непрекращающемся стремлении сделать документацию максимально простой и безболезненной, я начал экспериментировать с библиотекой команды Facebook под названием react-docgen.

Это интерфейс командной строки и API, которые считывают файлы компонентов React, собирают всю документацию, оставленную в комментариях, и выкачивают объект со всеми документами и реквизитами:

{
  "props": {
    "foo": {
      "type": {
        "name": "number"
      },
      "required": false,
      "description": "Description of prop \"foo\".",
      "defaultValue": {
        "value": "42",
        "computed": false
      }
    },
    "bar": {
      "type": {
        "name": "custom"
      },
      "required": false,
      "description": "Description of prop \"bar\" (a custom validation function).",
      "defaultValue": {
        "value": "21",
        "computed": false
      }
    }
  },
  "description": "General component description."
}

Некоторые системы проектирования и фреймворки используют react-docgen как часть процесса создания документации. Обычно вы берете объект, выводимый CLI, и сохраняете его в файл JSON для использования в других скриптах / инструментах. Отлично подходит для создания файлов содержимого / Markdown с использованием SSG (генераторов статических сайтов), таких как GatsbyJS, Jekyll или Slate.

Я протестировал два способа использования react-docgen для документирования кода React. Моей целью было использовать GatsbyJS в качестве генератора статических сайтов и создать сайт документации на основе данных интерфейса командной строки.

Если вас интересует более нестандартное решение, попробуйте react-styleguidist. Он использует response-docgen для создания одностраничной документации по всем компонентам.

Приступим ⚡️

Процесс

Я описал базовый процесс выше: мы хотим запустить скрипт, который использует response-docgen API, и сгенерировать файл JSON с документами React, который мы можем использовать в процессе сборки сайта.

Давайте сначала попробуем это 👇

Инструментальный скрипт + вывод Gatsby

Процесс довольно прост:

  1. Создайте скрипт, который захватывает все компоненты и использует API-интерфейс response-docgen для синтаксического анализа файлов в JSON (сохраняется как выходной файл - components.json)
  2. GatsbyJS gatsby-node.js скрипт для анализа JSON и создания страниц.

Сначала делаем скрипт сборки:

const fs = require("fs");
const path = require("path");
const reactDocs = require("react-docgen");
// The React components to load
const componentFolder = "./src/components/";
// Where the JSON file ends up
const componentJsonPath = "./docs/components.json";
const componentDataArray = [];
function pushComponent(component) {
  componentDataArray.push(component);
}
function createComponentFile() {
  const componentJsonArray = JSON.stringify(componentDataArray, null, 2);
  fs.writeFile(componentJsonPath, componentJsonArray, "utf8", (err, data) => {
    if (err) {
      throw err;
    }
    console.log("Created component file");
  });
}
/**
 * Use React-Docgen to parse the loaded component
 * into JS object of props, comments
 *
 * @param {File} component
 * @param {String} filename
 */
function parseComponent(component, filename) {
  const componentInfo = reactDocs.parse(component);
  const splitIndex = filename.indexOf("/src/");
  const shortname = filename.substring(splitIndex + 4);
  componentInfo.filename = shortname;
  pushComponent(componentInfo);
}
/**
 * Loads a component file, then runs parsing callback
 * @param {String} file
 * @param {Promise} resolve
 */
function loadComponent(file, resolve) {
  fs.readFile(file, (err, data) => {
    if (err) {
      throw err;
    }
    // Parse the component into JS object
    resolve(parseComponent(data, file));
  });
}
/**
 * Explores recursively a directory and returns all the filepaths and folderpaths in the callback.
 *
 * @see http://stackoverflow.com/a/5827895/4241030
 * @param {String} dir
 * @param {Function} done
 */
function filewalker(dir, done) {
  let results = [];
  fs.readdir(dir, async (err, list) => {
    if (err) return done(err);
    let pending = list.length;
    if (!pending) return done(null, results);
    list.forEach(file => {
      file = path.resolve(dir, file);
      fs.stat(file, async (err, stat) => {
        // If directory, execute a recursive call
        if (stat && stat.isDirectory()) {
          filewalker(file, (err, res) => {
            results = results.concat(res);
            if (!--pending) done(null, results);
          });
        } else {
          // Check if is a Javascript file
          // And not a story or test
          if (
            file.endsWith(".js") &&
            !file.endsWith(".story.js") &&
            !file.endsWith(".test.js")
          ) {
            await new Promise(resolve => {
              loadComponent(file, resolve);
            });
            await results.push(file);
          }
          if (!--pending) done(null, results);
        }
      });
    });
  });
}
filewalker(componentFolder, (err, data) => {
  if (err) {
    throw err;
  }
  createComponentFile();
});

Мы используем функцию firewalker(), которую я нашел на Github, которая загружает папку и «просматривает» каждый файл. Когда мы просматриваем каждый файл, мы проверяем, является ли он JS-файлом (а не тестовым или JS-файлом Storybook), а затем запускаем функцию loadComponent(), которая является оболочкой для Node API для загрузки файлов.

Как только файл компонента действительно загружен, мы запускаем parseComponent()функцию, которая фактически запускает response-docgen для нашего файла. Наконец, мы «помещаем» сгенерированные данные документов в массив. После загрузки всех файлов наша firewalker()функция имеет обратный вызов, который запускает функцию createComponentFile(), которая выводит фактический файл JSON.

Используя скрипт

Теперь мы можем сгенерировать JSON-файл всех наших компонентов внутри /src/components/, запустив скрипт в интерфейсе командной строки Node:

node generate-documentation.js

GatsbyJS поддерживает использование файлов JSON в качестве источника для создания сайтов с использованием gatsby-transformer-json. Когда мы загружаем сгенерированный файл, он преобразует JSON в узел GraphQL, который мы можем запросить.

Чистый Гэтсби

Последний метод работает, но он казался громоздким и подверженным ошибкам из-за необходимости полагаться на отдельный скрипт инструментов. После небольшого исследования я обнаружил второй, более интегрированный способ справиться с этим.

  1. Используйте плагин response-docgen для Gatsby, который захватывает все компоненты из определенной папки, генерирует JSON и запускает для него конечную точку GraphQL.

Мы устанавливаем плагин и добавляем его в нашу конфигурацию Gatsby (вместе с источником файловой системы, который импортирует наши компоненты):

gatsby-config.js:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `components`,
        // Location of your React components
        path: `../src/components/`,
      },
    },
    // "Transforms" our "source" of React/JS files with the react-docgen CLI
    // and creates a GraphQL node with the output
    `gatsby-transformer-react-docgen`,
  ],
}

А затем отобразить наши данные так же просто, как запрос GraphQL:

import React, { Component } from 'react'
import { graphql } from 'gatsby'
import Layout from '../components/layout'
import PropsTable from '../components/propstable'
export default class ComponentPage extends Component {
  render() {
    const { children, data, tableOfContents } = this.props
    console.log('mdx', data.mdx)
    console.log('component metadata', data.componentMetadata)
    return (
      <Layout>
        <div className="content">
          {children}
          <h1>{data.componentMetadata.displayName}</h1>
          <p>{data.componentMetadata.docblock}</p>
          <h2 style={{ marginTop: '2rem' }}>Props:</h2>
          <PropsTable
            propMetaData={data.componentMetadata.childrenComponentProp}
          />
        </div>
      </Layout>
    )
  }
}
export const pageQuery = graphql`
  query($name: String!) {
    componentMetadata(displayName: { eq: $name }) {
      id
      displayName
      docblock
      doclets
      childrenComponentProp {
        name
        docblock
        required
        parentType {
          name
        }
        type {
          value
        }
        defaultValue {
          value
          computed
        }
      }
      composes
    }
  }
`

Бонус: стол реквизита

Чтобы отобразить все данные из наших реквизитов (описание, значение по умолчанию, требуется ли это? И т. Д.), Мы создаем компонент, который принимает наши реквизиты из интерфейса командной строки react-docgen и выводит таблицу. Я нашел это на Github и изменил его для работы с Gatsby-версией response-docgen:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
/**
 * Table for React props generated by react-docgen
 *
 * @see https://github.com/episodeyang/react-component-props-table/
 */
const PropsTable = props => {
  let { className = '', propMetaData = [], ..._props } = props
  if (className) className += ' component-props-table'
  return (
    <table className={className} {..._props}>
      <thead>
        <tr>
          <th>Prop Name</th>
          <th>Type</th>
          <th>Is Required</th>
          <th>Default Value</th>
          <th>Description</th>
        </tr>
      </thead>
      <tbody>
        {Object.keys(propMetaData).map(key => {
          const prop = propMetaData[key]
          return (
            <tr key={key}>
              <td style={{ color: 'rgb(17, 147, 154)' }}>{prop.name}</td>
              <td>{prop.parentType ? prop.parentType.name : ''}</td>
              {prop.required ? (
                <td style={{ color: 'rgb(255, 76, 34)' }}>required</td>
              ) : (
                <td style={{ color: '#c6c6c6' }}>optional</td>
              )}
              {prop.defaultValue ? (
                <td style={{ color: 'rgb(236, 171, 32)' }}>
                  {prop.defaultValue.value}
                </td>
              ) : (
                <td style={{ color: '#c6c6c6' }}>none</td>
              )}
              {prop.docblock ? <td>{prop.docblock}</td> : <td />}
            </tr>
          )
        })}
      </tbody>
    </table>
  )
}
PropsTable.propTypes = {
  /** this is the `metadata.props` field of what metadata you get from the react-docgen-loader.  */
  propMetaData: PropTypes.object,
}
PropsTable.defaultProps = {
  propMetaData: {},
}
export default PropsTable

Намного более эффективно, поскольку он запускает response-docgen при сборке, вместо того, чтобы требовать от нас запускать скрипт отдельно (или подключать его к нашему процессу сборки).

Также импортирует документацию как конечную точку GraphQL, что позволяет нам запрашивать данные - вместо жесткого импорта (потенциально гигантского) файла JSON - или с помощью плагина gatsby-transformer-json (который не форматирует данные для GraphQL, а также специальный плагин react-docgen).

Вы можете скачать финальную версию чистого Гэтсби на Github здесь: Gatsby Documentation Starter.

Получение документации

Я надеюсь, что это помогло вам понять процесс, лежащий в основе документации или, в частности, документации по компонентам React. Использование интерфейса командной строки или API-интерфейса react-docgen делает процесс таким же простым, как загрузка файлов компонентов и их передача через библиотеку, выкачивая структурированные данные, идеально подходящие для интерфейсов внешнего интерфейса.

Существует множество готовых решений, но всегда полезно понимать, как они работают (демистификация магии ✨), особенно если вы хотите создать что-то новое и свежее (пока только фреймворки).

Привет,
Рё

Ссылки: