loading...

Design System creation, from scratch to npmjs.com - Part 1

adancarrasco profile image Adán Carrasco ・6 min read

Nowadays having a Design System is pretty common. It offers really good advantages: all your projects share the same components and you have a gallery where everyone can see them, even non technical people.

TLDR; I want my clone! 😬

In part 1, I will show you how to setup the project with React + TypeScript + Rollup.

By the end of this series you will have a Design System (DS) created using React + TypeScript + Rollup, and not only that; you will create a DS following Atomic Design methodology. If you are not familiar with Atomic Design don't worry about it, you'll get it by the end of this series.

Also your DS will be ready to be published to npm, this will allow you to import it in multiple projects and create your products faster than never. 😎

Prerequisites:

  1. Have npm installed
  2. Have a text editor, preferably VSCode
  3. Optional: If you want to publish it, you'll need an npm account

Hands on:

Let's start giving this project some shape! I don't intent to do this series really tedious and containing a lot of things that it shouldn't, for that I will be explaining briefly the sections that are not mandatory to know (deeply) to setup the boilerplate. However, if you have any questions you can always drop a comment and I will reply any doubts you may have. 😃

Let's start! 💪

1. Create the project folder and init npm

Let's start by creating a folder for our Project. In our project's folder we do:

npm init -y

This command initializes an npm project for us with the default settings.

2. Installing the packages we need

Now let's install the packages we need. The following is the list of packages needed to setup the DS boilerplate. It includes packages to work with React + TypeScript (and compatibility with it), integrate testing using Jest + Testing Library, also includes the minimum packages to setup Rollup. As well as the minimum requirements to transpile our code to work with old browsers using Babel. Something we normally use in modern projects is a linter, for this we will use ESlint + Prettier, and to visualize our DS we will use Storybook. The last and optional tool is EmotionJS to do our components in a Styled Components way.

  • TypeScript: yarn add -D typescript
  • React: yarn add -D react react-dom @types/react
  • Rollup: yarn add -D rollup rollup-plugin-copy rollup-plugin-peer-deps-external rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve
  • Testing: yarn add -D @testing-library/jest-dom @testing-library/react @types/jest jest ts-jest
  • For babel (transpiling): yarn add -D @babel/core babel-loader babel-preset-react-app identity-obj-proxy
  • EmotionJS (Styled Components): yarn add -D @emotion/core @emotion/styled babel-plugin-emotion
  • For Eslint and Prettier: yarn add -D eslint eslint-config-prettier eslint-plugin-prettier prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin
  • Storybook: yarn add -D @storybook/react

After installing the packages, we are set to start doing some configuration. ✨

3. Adding some scripts to run our project

Now in our package.json we need to put some scripts to let us build, test and visualize our components.

    "build": "rm -rf ./build && rollup -c",
    "lint": "eslint '*/**/*.{js,ts,tsx}' --quiet --fix",
    "storybook": "start-storybook -p 6006",
    "storybook:export": "build-storybook",
    "test": "jest",
    "test:watch": "jest --watch"

4. Setting up our packages configurations

All of the following need a configuration file to know how they should operate, depending on each project/team rules the config may change. For this example I will leave it as generic as possible, trying to affect the less I can. I will put at the beginning of the section the name of the config file for each of them.

Most of the properties in each config file are explained in the name of the prop, for some that are not obvious I will add a brief description at the end of the section. 😌

TypeScript

tsconfig.json

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "build",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom", "es2016", "es2017"],
    "sourceMap": true,
    "jsx": "react",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "typeRoots": ["./node_modules/@types", "jest"],
    "types": ["jest"]
  },
  "include": ["src/**/*"],
  "exclude": [
    "node_modules",
    "build",
    "src/**/*.stories.tsx",
    "src/**/*.test.tsx"
  ]
}

In summary this file will transpile our TypeScript into JavaScript using as target: es5, will ignore some folders (exclude), and will configure things for testing (typeRoots), using jsx for React, building everything in build's directory.

Rollup

rollup.config.js

import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import resolve from '@rollup/plugin-node-resolve'
import typescript from 'rollup-plugin-typescript2'
import commonjs from '@rollup/plugin-commonjs'

import packageJson from './package.json'

export default [
  {
    input: 'src/index.ts',
    output: [
      {
        file: packageJson.main,
        format: 'cjs',
        sourcemap: true,
      },
      {
        file: packageJson.module,
        format: 'esm',
        sourcemap: true,
      },
    ],
    plugins: [
      peerDepsExternal(),
      resolve(),
      commonjs(),
      typescript({ useTsconfigDeclarationDir: true }),
    ],
  },
]

This one is a little bit more extended, in this case we are telling rollup to use peerDepsExternal this means that if the project importing this DS already has installed the packages inside peerDepsExternal, they won't be included as part of this package (DS) import. It also setups the output format as CommonJS (csj) and ES modules (esm) (for older and modern browsers respectively). Using some plugins to do the transpilation for us.

Jest

jest.config.js

module.exports = {
  roots: ['./src'],
  setupFilesAfterEnv: ['./jest.setup.ts'],
  moduleFileExtensions: ['ts', 'tsx', 'js'],
  testPathIgnorePatterns: ['node_modules/'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  testMatch: ['**/*.test.(ts|tsx)'],
  moduleNameMapper: {
    // Mocks out all these file formats when tests are run
    '\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
}

This file is defining the files we are going to test, the format and how we are going to call them (testMath + moduleFileExtensions), how jest should interpret them (transform), also includes the folders that should be ignored (testPathIgnorePatterns) and finally some files/resources that will/can be mocked (moduleNameMapper). It also contains additional setup so we don't need to add it in all of our tests.

jest.setup.ts

import '@testing-library/jest-dom'

ESlint + Prettier

.eslintrc.js

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  settings: {
    react: {
      version: 'detect',
    },
    extends: [
      'plugin:react/recommended',
      'plugin:@typescript-eslint/recommended',
      'prettier/@typescript-eslint',
      'plugin:prettier/recommended',
    ],
    rules: {},
  },
}

Defines where the VSCode (in this case) will show you some errors, you can mix it up with TS Config to mark them as errors as well. In this file we have some recommended Lint rules for the tools we are using as React + TypeScript + Prettier.

.prettierrc.js

module.exports = {
  semi: false,
  trailingComma: 'all',
  singleQuote: true,
  printWidth: 100,
  tabWidth: 2,
}

This file is just matter of style for your code, if you want to use trailingCommas or not, singleQuotes for strings, your tabWidth, etc.

Storybook

.storybook/main.js

const path = require('path')

module.exports = {
  stories: ['../src/**/*.stories.tsx'],
  // Add any Storybook addons you want here: https://storybook.js.org/addons/
  addons: [],
  webpackFinal: async (config) => {
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      loader: require.resolve('babel-loader'),
      options: {
        presets: [['react-app', { flow: false, typescript: true }]],
      },
    })
    config.resolve.extensions.push('.ts', '.tsx')

    return config
  },
}

In this case we setup the name we will give to our stories (code where we will demo the usage of our components), the extensions and wether you are using Flow or TypeScript.

EmotionJS (Optional)

.babelrc

{
  "env": {
    "production": {
      "plugins": ["emotion"]
    },
    "development": {
      "plugins": [["emotion", { "sourceMap": true }]]
    }
  }
}

This is a small setup to tell our project how it will transpile our Emotion components.

End of part 1

In general all those config files have pretty much the same structure, it can be tricky at first but once you get familiar with the structure all of them are really alike.

I hope you enjoyed the first part of this series. IMO, the setup of the project is the more tedious, however the funniest parts are coming. 🎉

Thanks for reading, if you have any questions you can @ me in Twitter at @adancarrasco. See you in part 2!

Discussion

markdown guide