angularstorybooknrwlnrwl-nxangular-storybook

NX - UI Library with Storybook and individually exported components


I'm exploring Nx with Angular (relatively new to both) and trying to figure out how to generate a component library that:

So the ideal setup is sort of like this:

repo
 |--apps
    |--normal-app (can import just card or button, without pulling in both)
 |--libs
    |--ui (lists individually exportable components, and has a servable Storybook)
       |--.storybook
       |--card
       |--button

Following both a general Nx tutorial that showed how to create a shared component lib, as well as the guide for setting up Storybook in Nx, I generated a UI lib, and set up a Storybook schematic (if that's the right way to say it) for that lib directory.

The hope was to have all my shared components in one lib, and have Storybook set up to autogenerate and serve stories for each component -- but then be able to individually pull components from that lib into other applications without pulling in the whole, bulky UI library.

I successfully built out a UI lib and two components in it. I successfully set up Storybook in that lib. I then imported the UIModule from the index.ts file (the public API) in the UI lib, and used one of the two components in my template.

But then when I built the app that was importing the lib, the production build contained both components, although I had used one. This makes sense, since I'm importing the UiModule. This is, however, non-ideal.

I'm hoping to create a setup that allows for collocated components in a lib with Storybook setup, which can be imported individually.

Is there a way to do this without drastically altering the NX setup? I've explored two main options so far. One is to break all the components into their own UI libs, import them all into a separate app that is set up for Storybook, and import them individually into a main app. Like this (attempt #1):

repo
 |--apps
    |--storybook-app (imports all)
    |--other-app (imports just button)
 |--libs
    |--button
    |--card

That's non-ideal, though, for several reasons:

As a second attempt (#2), I tried to generate each of the components as separate libs in a shared directory:

repo
 |--apps
    |--storybook-app (imports all)
    |--other-app (imports just button)
 |--libs
    |--ui (just a directory; not a "project")
      |--button (module; has separate component dir beneath)
      |--card

This has a number of drawbacks of its own:

I could build out one of the versions described above (#2, probably), but I suspect there are best practices for what seems like a common use case. It seems that much of my confusion stems from the Angular dependency-injection. (In a React app, you could just import a component, but in Angular, each component belongs to a module and that, specifically, needs to be injected as well. Is it a bad practice to have component-specific modules?)

Does anyone know of an idiomatic/best-practice way to meet these ideal specifications (shared storybook library with individual exported components, or split components with automatically generated stories in NX)?


Solution

  • I haven't found a totally satisfactory solution for this problem, but here's what I ended up doing. It's close to #2 above:

    1. Created a libs/ui directory (not a project)
    2. Built a generator script that creates each component as its own separate module within that directory, and auto-creates a story scaffold in it.
    3. Created a component-lib lib. (Rather than an app, because Storybook has its own separate run commands).
    4. Set up a Storybook config in there using the Nx CLI.
    5. Altered the Storybook config to grab all .stories.ts files under the libs/ui directory.

    That way, the component-lib directory automatically adds each new component as its added, and I don't have to worry about a bunch of boilerplate whenever I add a component. Also, each component can be individually imported, without dragging in the bundle.

    I won't share my whole component-generator script, but it's a basic JS file that

    1. Runs the Nx library generator to generate a lib within the ui dir
    2. Runs the Nx component generator to generate a component within that lib.
    3. Uses fs to write an appropriate .stories.ts file in that lib.

    Then, in my component-lib directory, I modified the final line in .storybook/config.js to require all stories under the ui lib:

    configure(require.context('../../ui', true, /\.stories\.tsx?$/), module);
    

    It all feels a little hacky, particularly the generator script. But it works!