First of all you need to decide who will be your target consumers based on the following:
-
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).
-
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:
- You can use webpack/rollup.
- Choose rollup if you want to generate
commonjs
andesmodule
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
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 yourdesign-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 usewebpack
as their bundling tool.
- But this can be solved by setting
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'
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.
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
/individualButton.js
re-exports your command+click problem will be solved.
4. I have a component folder where I keep all the components. You import them as - import { Button } from 'my-design-system'. The entry point is 'src/index.js' where I import all the components from component folder and export them
-
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?