tuchk4 / storybook-readme

React Storybook addon to render README files in github style
MIT License
543 stars 250 forks source link

Is it possible to automatically add README's based on the module path? #83

Closed robcresswell closed 4 years ago

robcresswell commented 6 years ago

Is there any way to have the addon automatically add the local README.md? Given a consistent file structure, I was hoping to be able to just always load the README for a given story, without having to add the decorator to every single story.

For example, I use:

components/input
├── Input.vue
├── README.md
└── story.js

for every component.

tuchk4 commented 6 years ago

To implement this we should patch webpack config. The flow should be:

I would be happy for any help.

robcresswell commented 6 years ago

Work to do! Okay, I will keep this in mind. Thanks for the feedback. Not sure when / if I will get time to work on this, but I'll see what I can do.

kristofdombi commented 5 years ago

Yes, it's possible: I have done it like this:

config.js

import React from 'react'
import path from 'path'

import { ThemeDecorator } from './decorators/index'
import {
  configure,
  storiesOf,
  getStorybook,
  addDecorator,
} from '@storybook/react'
import { withKnobs } from '@storybook/addon-knobs/react'
import { withReadme } from 'storybook-readme'
import { withOptions } from '@storybook/addon-options'

const getPackageName = filePath =>
  path
    .dirname(filePath)
    .split(path.sep)
    .reverse()[1]

const req = require.context(
  '../../../components',
  true,
  /^((?!node_modules).)*\.example\.(js|tsx)$/
)

const readMeReq = require.context(
  '../../../components',
  true,
  /^((?!node_modules).)*\README\.md$/
)

addDecorator(
  withOptions({
    name: 'Components',
  })
)

configure(() => {
  req.keys().forEach(pathToExample => {
    const { name, Example, options = {} } = req(pathToExample)
    const packageName = getPackageName(pathToExample)
    const readmePath = readMeReq.keys().find(rm => rm.includes(packageName))
    const readme = readMeReq(readmePath)
    storiesOf(packageName, module)
      .addDecorator(withKnobs)
      .addDecorator(withReadme(readme))
      .addDecorator(withOptions(options))
      .addDecorator(ThemeDecorator)
      .add(name, () => <Example />, { options })
  })
}, module)

export { getStorybook }

HelloWorld.example.js:

import React from 'react'
import HelloWorld from 'path/to/hello-world'

export const name = 'Default'

export const Example = () => (
  <HelloWorld />
)
ndelangen commented 5 years ago

We're planning on making this a feature of addon-notes in the future too, so "it just works".

Great setup you discovered there @kristof0425 !

jephjohnson commented 5 years ago

@tuchk4 - Will this implementation mentioned above from @kristof0425 still work with v5? I am only getting it to work for one of my components?

Screen Shot 2019-03-16 at 6 13 58 PM Screen Shot 2019-03-16 at 6 16 15 PM
import "./styles.css";
import React from "react";
import path from "path";
import { getStorybook, storiesOf, configure } from "@storybook/react";
import { withKnobs } from "@storybook/addon-knobs";
import { addReadme } from 'storybook-readme'

let getComponentName = filePath =>
  path
    .dirname(filePath)
    .split(path.sep)
    .reverse()[0];

let getPackageName = filePath =>
  path
    .dirname(filePath)
    .split(path.sep)
    .reverse()[1];

configure(() => {

  // Automatically import all examples
  const req = require.context(
    "../packages",
    true,
    /^((?!node_modules).)*\.example\.js$/
  );

  const readMeReq = require.context(
    "../packages",
    true,
    /^((?!node_modules).)*\.README\.md$/
  )

  req.keys().forEach(pathToExample => {

    const { name, Example } = req(pathToExample);
    const packageName = getPackageName(pathToExample)
    const componentName = `${packageName}.${getComponentName(
      pathToExample
    )}`;

    const readmePath = readMeReq.keys().find(rm => rm.includes(packageName))
    const readme = readMeReq(readmePath)
    console.log(readmePath)

    storiesOf(componentName, module)
      .addDecorator(withKnobs)
      .addDecorator(addReadme)
      .addParameters({
        readme: {
          content: '<!-- STORY --><!-- PROPS -->',
          sidebar: readme,
        },
      })
      .add(name, () => <Example dummy="test" />);
  });
}, module);

export { getStorybook };
tuchk4 commented 5 years ago

@jephjohnson yes, it should work but maybe split into two lines. Not sure it will be parsed correctly )

   readme: {
          content: `
<!-- STORY -->
<!-- PROPS -->
`,
          sidebar: readme,
        },
      })
jephjohnson commented 5 years ago

@tuchk4 - Ah, stil no dice.

Screen Shot 2019-03-16 at 7 02 53 PM
tuchk4 commented 5 years ago

@jephjohnson

Here is working example:

import React from 'react';
import path from 'path';
import {
  getStorybook,
  storiesOf,
  addDecorator,
  configure,
} from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import { addReadme } from 'storybook-readme';

// register decorator
addDecorator(addReadme);

let getComponentName = filePath =>
  path
    .dirname(filePath)
    .split(path.sep)
    .reverse()[0];

let getPackageName = filePath =>
  path
    .dirname(filePath)
    .split(path.sep)
    .reverse()[1];

configure(() => {
  // Automatically import all examples
  const req = require.context(
    '../auto',
    true,
    /^((?!node_modules).)*\.example\.js$/,
  );

  const readMeReq = require.context(
    '../auto',
    true,
    /^((?!node_modules).)*\.README\.md$/,
  );

  req.keys().forEach(pathToExample => {
    const { name, Example } = req(pathToExample);
    const packageName = getPackageName(pathToExample);
    const componentName = `${packageName}.${getComponentName(pathToExample)}`;

    const readmePath = readMeReq.keys().find(rm => rm.includes(packageName));
    const readme = readMeReq(readmePath);

    storiesOf(componentName, module)
      .addParameters({
        readme: {
          content: '<!-- STORY --><!-- PROPS -->',
          sidebar: readme,
        },
      })
      .add(name, () => <Example dummy="test" />);
  });
}, module);

export { getStorybook };

Button.example.js

import React from 'react';
import Button from './';

export const name = 'Default';

export const Example = () => <Button label="Hi" />;

// copy proptypes so <!-- PROPS --> will work
Example.propTypes = Button.propTypes;
tuchk4 commented 5 years ago

image

jephjohnson commented 5 years ago

The decorator was in there. I’m thinking it’s related to my es lint folder. Still having the same issue of it not iterating through all the readmes

tuchk4 commented 5 years ago

Thinking that I should to add @kristof0425's solution to README and close this issue

kristofdombi commented 5 years ago

That would be awesome! 🎉 @tuchk4 Also thanks for the v5 support!

tuchk4 commented 5 years ago

@kristof0425 Need you advice :) Also ping @jephjohnson as I know you use such way to load stories with docs.

I would like to add to docs how to automatically add README based on the story path. Here is PR - https://github.com/tuchk4/storybook-readme/pull/166

When tests faced the problem:

require.context is not a function

In your example this function is used to get all stories and README files by specific pattern.

Found this issue https://github.com/storybooks/storybook/issues/4479

Do we need to install additional babel plugins to use your solution?

kristofdombi commented 5 years ago

Currently, this is the setup we use for Storybook:

Package Version
webpack v4.28.4
@storybook/react v5.0.5
storybook-readme v5.0.1
babel-loader v8.0.0
Webpack config ```js const themeOverride = require('../webpack.config.theme.override') process.env.NODE_ENV = 'development' module.exports = { resolve: { extensions: ['.mjs', '.ts', '.tsx', '.js'], }, module: { rules: [ { test: /\.(js|ts|tsx)$/, use: ['babel-loader'], }, { test: /\.svg$/, loader: 'file-loader', }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, { test: /\.less$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader', }, { loader: 'less-loader', options: { modifyVars: themeOverride, javascriptEnabled: true, }, }, ], }, ], }, devtool: 'source-map', } ```
Storybook config ```js import React from 'react' import path from 'path' import { ThemeDecorator } from './decorators/index' import { configure, storiesOf, getStorybook, addParameters, } from '@storybook/react' import { withKnobs } from '@storybook/addon-knobs/react' import { addReadme } from 'storybook-readme' const getPackageName = filePath => path .dirname(filePath) .split(path.sep) .reverse()[1] const getPathToExample = path => { let hasReachedExamples = false return path .split('/') .filter((folder, i) => { // first two folders are common if (folder === 'examples') hasReachedExamples = true if (i < 2 || hasReachedExamples || folder.includes('.tsx')) return false else return true }) .join('/') .replace(/.example.(js|tsx)/, '') } const req = require.context( '../../../components', true, /^((?!node_modules).)*\.example\.(js|tsx)$/ ) const readMeReq = require.context( '../../../components', true, /^((?!node_modules).)*\README\.md$/ ) addParameters({ name: 'MK-Components', }) configure(() => { req.keys().forEach(pathToExample => { const { name, Example, options = {} } = req(pathToExample) const packageName = getPackageName(pathToExample) const readmePath = readMeReq.keys().find(rm => rm.includes(packageName)) const readme = readMeReq(readmePath) const path = getPathToExample(pathToExample) storiesOf(path, module) .addDecorator( withKnobs({ escapeHTML: false, }) ) .addDecorator(addReadme) .addParameters({ readme: { sidebar: readme, }, }) .addParameters(options) .addDecorator(ThemeDecorator) .add(name, () => , { options }) }) }, module) export { getStorybook } ```
Babel config (I'm not sure whether it's relevant to this problem) ```js module.exports = { presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-flow', '@babel/typescript', ], plugins: [ '@babel/plugin-transform-async-to-generator', [ 'import', { libraryName: 'antd', style: true, }, ], 'react-hot-loader/babel', '@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-import-meta', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-json-strings', [ '@babel/plugin-proposal-decorators', { legacy: true, }, ], '@babel/plugin-proposal-function-sent', '@babel/plugin-proposal-export-namespace-from', '@babel/plugin-proposal-numeric-separator', '@babel/plugin-proposal-throw-expressions', '@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-logical-assignment-operators', '@babel/plugin-proposal-optional-chaining', [ '@babel/plugin-proposal-pipeline-operator', { proposal: 'minimal', }, ], '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-proposal-do-expressions', '@babel/plugin-proposal-function-bind', ], env: { test: { plugins: [ 'emotion', [ '@babel/plugin-transform-modules-commonjs', { loose: true, }, ], ], }, development: { plugins: [ ['emotion', { sourceMap: true, autoLabel: true }], '@babel/plugin-transform-modules-commonjs', 'babel-plugin-dynamic-import-node', ], }, production: { plugins: [ ['emotion', { hoist: true }], '@babel/plugin-transform-modules-commonjs', ], }, }, } ```

@tuchk4 If you could share with me how you test the package, I can dig into the problem myself as well. 🙂

jephjohnson commented 5 years ago

@kristof0425 @tuchk4 - Hey guys, sorry for the delay...Any new feedback on this? Still haven't got it resolved on my end. Here is the pakage.json.

{
  "name": "@demo",
  "private": true,
  "license": "ISC",
  "scripts": {
    "start": "start-storybook -p 9001 -c .storybook",
    "test": "jest -c jest.config.unit.js",
    "test:watch": "jest -c jest.config.unit.js --watch",
    "test:visual": "jest -c jest.config.visual-regression.js",
    "precommit": "pretty-quick --staged",
    "lint": "eslint -c .eslintrc.json 'packages/**/*.js'",
    "build-storybook": "build-storybook -c .storybook -o .out"
  },
  "devDependencies": {
    "0": "^0.0.0",
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.3.4",
    "@babel/plugin-external-helpers": "^7.2.0",
    "@babel/plugin-proposal-class-properties": "^7.3.4",
    "@babel/plugin-proposal-object-rest-spread": "^7.3.4",
    "@babel/plugin-transform-object-assign": "^7.2.0",
    "@babel/plugin-transform-react-jsx": "^7.3.0",
    "@babel/polyfill": "^7.4.3",
    "@babel/preset-env": "^7.3.4",
    "@babel/preset-react": "^7.0.0",
    "@storybook/addon-a11y": "^5.0.6",
    "@storybook/addon-actions": "^5.0.1",
    "@storybook/addon-knobs": "^5.0.1",
    "@storybook/addon-storysource": "^5.0.1",
    "@storybook/react": "^5.0.6",
    "autoprefixer": "^9.2.1",
    "babel-eslint": "10.0.1",
    "babel-jest": "^24.5.0",
    "babel-loader": "^8.0.5",
    "eslint": "^5.15.3",
    "eslint-config-prettier": "^4.1.0",
    "eslint-config-react-app": "^3.0.4",
    "eslint-loader": "2.1.1",
    "eslint-plugin-flowtype": "3.0.0",
    "eslint-plugin-import": "2.14.0",
    "eslint-plugin-jsx-a11y": "6.1.2",
    "eslint-plugin-prettier": "^3.0.1",
    "eslint-plugin-react": "7.11.1",
    "gzip-size": "^5.0.0",
    "html-webpack-plugin": "^3.2.0",
    "husky": "^1.1.2",
    "jest": "24.4.0",
    "jest-dom": "^3.1.3",
    "jest-puppeteer-react": "^4.6.1",
    "prettier": "^1.14.3",
    "pretty-bytes": "^5.1.0",
    "pretty-quick": "^1.8.0",
    "puppeteer": "^1.14.0",
    "react": "^16.5.2",
    "react-dom": "^16.5.2",
    "react-testing-library": "^6.0.0",
    "rollup": "^0.66.6",
    "rollup-plugin-babel": "^4.0.3",
    "rollup-plugin-commonjs": "^9.2.0",
    "rollup-plugin-node-resolve": "^3.4.0",
    "rollup-plugin-replace": "^2.1.0",
    "rollup-plugin-uglify": "^6.0.0",
    "storybook-readme": "^5.0.2",
    "user-event": "^1.4.7"
  },
  "prettier": {},
  "husky": {
    "hooks": {
      "pre-push": "yarn run lint"
    }
  },
  "dependencies": {
    "classnames": "^2.2.6",
    "prop-types": "^15.7.2"
  }
}
ndelangen commented 5 years ago

Hey @tuchk4 This would become a much easier process once storybook config has changed to monoconfig:

https://github.com/storybooks/storybook/issues/6806

tuchk4 commented 5 years ago

@ndelangen nice new feature! Seems it will be possible to implement awesome new features much easier :)

fgaleano commented 5 years ago

@tuchk4 I put together a repo that demonstrates the error @jephjohnson has been describing.

https://github.com/fgaleano/readme

Just npm install on root and then npm start.

The main problem is that prop's type, description and defaultValue are not picked up even though they are present in the component's props definition.

You can see Storybook's configuration in .storybook/config.js and the component props at packages/core/src/button/index.js. Then all of the examples have the same configuration as far as exporting props.

Let us know if you can reproduce the error as I'm describing it.

Thank you!

jephjohnson commented 5 years ago

@kristof0425 @tuchk4 - Any thoughts on the repo above? Thanks, Jeph

tuchk4 commented 5 years ago

@jephjohnson sorry for the delay, had hard working days. going to back to storybook-readme in nearest days.

jephjohnson commented 5 years ago

@jephjohnson sorry for the delay, had hard working days. going to back to storybook-readme in nearest days.

Hi @tuchk4 - Just touching base on this. Anything new? Thanks, J