Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save tiagobbraga/74047f5bcf390e16a015ad1429eaff1f to your computer and use it in GitHub Desktop.
Save tiagobbraga/74047f5bcf390e16a015ad1429eaff1f to your computer and use it in GitHub Desktop.

Bundling Design Systems/Component Libraries

First of all you need to decide who will be your target consumers based on the following:

  1. They have the same environment(webpack config, babel config) setup as you where you built your design system(this is mostly possible if you use monorepos/same configs where all the teams share the same environment).

  2. They don't have the same environment which is the case when you work in bigger teams and you want to distribute your design system as any other npm package which is already built and can be used directly.

If your use case falls under case no. 1 then you can just compile the source babel src -d build and leave the bundling to the consumer projects tools(webpack/rollup)

design-system
  src
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js
  build
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js
  Button.js // this is a re-export from build
    export default from './build/Button'

and then in your consumer projects

// app.js
import Button from 'design-system/Button'

if your use case falls under case no. 2 then you need to be careful about many things so that your consumer don't end up importing unnecessary things.

Let me start with what tool to use:

  1. You can use webpack/rollup.
  • Choose rollup if you want to generate commonjs and esmodule version of your components.
  • Choose webpack if you don't care about esmodule and don't want to invest in understanding rollup(P.S it's simple enough and it's worth investing time in it everything is plugins. esp when you're building libraries).

Let's understand this with an example. Assume this is your source code directory structure

design-system
  src
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js

With Webpack

Your webpack config will look something like this

module.exports = {
  entry: [
    './src/Button/index.js',
    './src/Text/index.js',
  ]
  
  output: {
    libraryTarget: 'commonjs',
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
  },
  
  externals: Object.keys(peerDependencies).reduce((acc,peerDependency)=> {...acc, [peerDependency]: peerDependency}, {})
}

if you want your css files to be extracted out in a seaprate file per JS file you can use mini-css-extract-plugin

After you run this you'll have something like this

design-system
  src
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js
  build
    Button.js
    Text.js

Once you publish this you can't have main field in your package.json as there's no single entry point and all your users have to do this

import Button from 'design-system/build/Button'

which is kind of bad DX and looks weird as well.

This can be solved in multiple ways:

1. You can have a top level index.js which is nothing but re-exports of all your components(this can generated using a simple script as part of your build process)

design-system
  src
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js
  build
    Button.js
    Text.js
  index.js
    export { default as Button } from './build/Button'
    export { default as Text } from './build/Text'
  • So now your consumers can do like this
import { Button, Text } from 'design-system'
  • But still this will make your imports ugly since everything is being imported from design-system in one line and everything is named import. Imagine importing 10 components in single line.
  • Plus most commonly people will think that you can't have tree shaking since webpack knows everything is a named import from design-system/index.js so it can't statically analyze what to tree shake.
    • But this can be solved by setting side-effects: false in your design-system/package.json so webpack knows this can be tree-shaken and it doesn't has any side-effects. But this needs a gaurantee that your consumer tools should use webpack as their bundling tool.

2. You can have individual components at the root Button.js and re-export Button from build/Button.js

  • This is pretty neat
design-system
  src
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js
  build
    Button.js
    Text.js
  Button.js
    export default from './build/Button'
  Text.js
    export default from './build/Text'
  • So now your consumers can import like this
import Button from 'design-system/Button'
import Text from 'design-system/Text'

With Rollup

If you want to target commonjs and esmodule then you can do that with rollup

const config = fs
  .readdirSync('src')
  .map((component) => ({
    input: `src/${component}/index.js`,
    external: Object.keys(peerDependencies),
    plugins: [
      babel({
        exclude: 'node_modules/**',
        runtimeHelpers: true,
        externalHelpers: true,
      }),
      commonjs(),
      json(),
    ],
    output: [{
      file: `cjs/${component}.js`,
      format: 'cjs',
    }, {
      file: `${component}.js`,
      format: 'esm',
    }],
  }));
export default config

So after you run rollup you'll have somthing like this

design-system
  src
    Button
      Button.js
      index.js
    Text
      Text.js
      index.js
  cjs
    Button.js
    Text.js
  Button.js
  Text.js

Now the obvious question to ask is why cjs as a separate folder and esm at the root level. This is not the only way to do it. But the idea here is that if your majority of the consumers are mostly concerned with the esm version then the DX becomes simpler for them.

import Button from 'design-system/Button'

and the ones(minority/legacy codebase) who are concerned about cjs they can do like this

import Button from 'design-system/cjs/Button'

So there are multiple ways of doing things depends what fits your use case. We use a good mix and match of all the above mentioned things based on use cases. Also if you look at Leaf-UI we just transpile the source and leave the bundling to the consumers. You can read the script for re-exporting here.

For some basics around setup and monorepos watch this video

screenshot 2019-01-24 at 12 00 12 pm

Now let me answer your questions one by one.

  • We use styled-components and we don't extract css out in a separate file.
  • If you use mini-css-extract-plugin you can re-export it from your Button component and refer it in your consumer component directly
import Button, { buttonStyles } from 'design-system/Button' 
  • Bundle per component. Helps to separate out concerns when importing components(default and named imports).
  • For individual component bundles refer this webpack config
  • If you follow re-export on the root from index.js/individual Button.js re-exports your command+click problem will be solved.
  • Question: Possible to achieve import Button from 'my-design-system/Button' with current project structure

    • You can refer the whole process as mentioned above for this.
  • Question: libraryTarget in Webpack is 'commonjs2'. What you use?

    • commonjs. It's a minute difference shouldn't matter much. Read here about it more.
  • Question: Is it good practice to refer output path in Webpack from build folder?

    • Need more details about this. Can you elaborate?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment