Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: PostVisit hook unable to access story source code #489

Closed
luciob opened this issue Jul 5, 2024 · 7 comments
Closed

[Bug]: PostVisit hook unable to access story source code #489

luciob opened this issue Jul 5, 2024 · 7 comments
Labels
question Further information is requested

Comments

@luciob
Copy link

luciob commented Jul 5, 2024

Describe the bug

Hello, I'm using the postVisit hook of test-runner to generate a JSON file containing the source code (in TSX) of each story available in my codebase.

In test-runner.ts I have the following snippet of code:

  async postVisit(page, context) {
    ...
    const { parameters } = await getStoryContext(page, context);
    console.log(parameters.docs.source.originalSource);
    ...
  }

I have a general setting that tells storybook how to render docs.source.type that was initially set to dynamic.
With this initial setting the source docs generated in the StoryBook web app are fine:

dynamic-source-web

While the values retrieved by test-runner in postVisit method are the following:

      {
        args: {
          value: true
        }
      }

If the type is set to auto it behaves like dynamic.
If I set the type to code the web app source code shows args:

code-source-web

While the output of test-runner remains the same of above.

Seems like there is no option to have on both the web and the postVisit the string source code.
How can I achieve this?
Is there another way to programmatically catch and save to a JSON file the source code of a story?
Thanks

To Reproduce

Clone the repo and checkout branch 417-chore-add-tool-to-generate-components-database.

The source code of the branch is available here

Run:

  • nvm use
  • npm i
  • open one terminal and run: npm start
  • open one side terminal and run npm run test:db

The test:db command will output to the console an example of the source code.

System

Storybook Environment Info:

  System:
    OS: macOS 14.2.1
    CPU: (8) arm64 Apple M1 Pro
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.14.0 - ~/.nvm/versions/node/v20.14.0/bin/node
    npm: 10.8.1 - ~/Works/melfore/libraries/mosaic/node_modules/.bin/npm <----- active
  Browsers:
    Chrome: 126.0.6478.127
    Safari: 17.2.1
  npmPackages:
    @storybook/addon-actions: ^8.1.10 => 8.1.10 
    @storybook/addon-coverage: ^1.0.4 => 1.0.4 
    @storybook/addon-essentials: ^8.1.10 => 8.1.10 
    @storybook/addon-interactions: ^8.1.10 => 8.1.10 
    @storybook/addon-links: ^8.1.10 => 8.1.10 
    @storybook/addon-themes: ^8.1.10 => 8.1.10 
    @storybook/addon-webpack5-compiler-babel: ^3.0.3 => 3.0.3 
    @storybook/react: ^8.1.10 => 8.1.10 
    @storybook/react-webpack5: ^8.1.10 => 8.1.10 
    @storybook/test: ^8.1.10 => 8.1.10 
    @storybook/test-runner: ^0.18.2 => 0.18.2 
    eslint-plugin-storybook: ^0.8.0 => 0.8.0 
    storybook: ^8.1.10 => 8.1.10

Additional context

No response

@yannbf
Copy link
Member

yannbf commented Jul 10, 2024

Hey @luciob what you are trying to achieve is not possible.
Storybook does not hold the JSX source in the parameters. The Storybook docs addon uses react-element-to-jsx-string internally to rebuild the JSX and that happens dynamically as you access the docs view in Storybook.

If you want, the test-runner can generate DOM snapshots instead. I am not sure what you are trying to achieve though, would be nice to understand your use case better.

@luciob
Copy link
Author

luciob commented Jul 10, 2024

Hi @yannbf, thanks for your reply.
I'm already doing snapshots in the postVisit hook, following the tutorial available on SB documentation.
What I'm trying to achieve is to build a JSON database of all components in my library, using StoryBook stories as source of truth.
For each component I want to have a record with:

  1. name
  2. description
  3. props (array of structured data)
  4. examples (array of source code examples, 1 for each component story)

The first three were quite easy using the postVisit hook to access story parameters.
The last one is the missing one I'm trying to achieve.

Maybe I could use the react-element-to-jsx-string somehow, but that's not clear to me how to dynamically import the components.

@yannbf
Copy link
Member

yannbf commented Jul 10, 2024

I see! So the examples you are looking for are of the JSX, right? e.g.

<Checkbox onChange={...} />

I'm honestly not sure how you could achieve that with the test-runner and how difficult it might be, or even if it's possible. You could take some inspiration from how it's done in Storybook docs:
https://github.com/storybookjs/storybook/blob/3a1e61cefec8dfd350d42db5598b2d0d9159a878/code/renderers/react/src/docs/jsxDecorator.tsx#L78

Just keep in mind that the test-runner hook runs in node, so if you need to run things in the browser, you need to use page.evaluate.

The only way to get this easily would be to manually add the source as a parameter, and then access it, but then it will be quite some effort for you to keep things up to date.

@yannbf
Copy link
Member

yannbf commented Jul 11, 2024

Alright I found a solution to your use case. Please be advised that this is not a recommendation at all, this is more like a hacky workaround that happens to work well! However this code is brittle as it depends on internal things that might change.

Storybook already does the job for you, and addon-docs emits the story source after it's computed. So here's an example of how to:

  • listen to that event before the story is rendered (preVisit hook)
  • store the data
  • use it to do whatever you want (postVisit hook)
import { getStoryContext, TestRunnerConfig } from '@storybook/test-runner'

declare global {
  // this is defined internally in the test-runner
  // and can be used for logging from the browser to node, helpful for debugging
  var logToPage: (message: string) => Promise<void>;
}

type SourceData = { source: string; args: Record<string, any>; id: string}
let extractedData: SourceData

const waitFor = async <T>(condition: () => T): Promise<T> => {
  let result = condition();
  let timeout = 2000;
  const interval = 100;

  while (!result && timeout > 0) {
    await new Promise(resolve => setTimeout(resolve, interval));
    result = condition();
    timeout -= interval;
  }

  if (!result) {
    throw new Error('Timeout exceeded');
  }

  return result;
};

const config: TestRunnerConfig = {
  async preVisit(page) {
    const extractData = page.evaluate<SourceData>(() => {
      return new Promise((resolve, reject) => {
        const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__;
        // use this helper if you want to debug
        // window.logToPage('listening to the storybook channel')

        // event emitted by the addon-docs when a snippet is rendered
        channel.on('@storybook/core/docs/snippet-rendered', (data) => {
          resolve(data)
        })
      })
    })
    // we can't await this because it will block the test from continuing
    // so we store its value and get it in the next hook
    extractData.then(data => {
      extractedData = data
    })
  },
  async postVisit(_page, context) {
    // waitFor is probably not needed so feel free to remove it. It's here to ensure that the data will be there
    // there could be cases where timing isn't right and the event takes a bit longer to be emitted (e.g. complex components that take longer to compute)
    const data = await waitFor<typeof extractedData>(() => extractedData)
    console.log('Story data', {
      id: context.id,
      title: context.title,
      name: context.name,
      source: data.source,
      props: data.args
    })
  }
}

export default config

This results in what you're looking for:

{
        id: 'components-iconbutton--default',
        title: 'Components/IconButton',
        name: 'Default',
        source: '<IconButton\n  aria-label="forward"\n  name="arrow-right"\n/>',
        props: { name: 'arrow-right', small: false, 'aria-label': 'forward' }
}

There are things you can change in this snippet. Some things are there for debugging purposes, some things might not be needed, I just wanted to give you a baseline to start with!

@yannbf yannbf added question Further information is requested and removed bug Something isn't working needs triage labels Jul 11, 2024
@yannbf yannbf closed this as completed Jul 11, 2024
@luciob
Copy link
Author

luciob commented Jul 11, 2024

Thanks @yannbf that worked. I was able to solve the issue and generate the components DB.
Just one side note: addon-docs must be explicitly installed as dev dependency.

@yannbf
Copy link
Member

yannbf commented Jul 11, 2024

Great to hear @luciob! I'd really love to see what you are able to achieve with this. Seems like you're experimenting with AI?

@luciob
Copy link
Author

luciob commented Jul 11, 2024

Great to hear @luciob! I'd really love to see what you are able to achieve with this. Seems like you're experimenting with AI?

That is the idea. Build a JSON components DB to have all possible documentation for our MUI wrapper library @melfore/mosaic. Once that is done, create something very similar to projects like openV0 to let GPT generate UIs sourcing from our own library. Let's see where this will bring us..

Thanks again for your precious support!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants