Sharing React components with rollup.js

July 24, 2019 – Thibaud Esnouf – 7-minute read

This article was written before Drivy was acquired by Getaround, and became Getaround EU. Some references to Drivy may therefore remain in the post

At Drivy, we have defined our very own design system. This system describes our visual guidelines and rules, and is composed of visual web components. For each component, we have created a React implementation that could easily be used by our design team to build a web site documentation (thanks to MDX).

Having a documented design system was an achievement in itself. But to fully take advantage of it, the final step was to use our React components in other frontend projects.

In a Node.js world, that means importing them as a node module dependency.

The main steps are:

  1. bundle the components in a useable manner
  2. publish the result through NPM

Project characteristics

Key points of the design system project to bundle:

  • React components (tsx files)
  • TypeScript (ts files)
  • SVG assets
  • Sass classes and utilities
  • Design tokens (single source of truth variables stored in JSON files, used to propagate our design decisions)

Notice that some of the sources have a specific syntax (tsx/ts files) and have to be transformed (transpiled) to be read by a browser. Bundling process must produce outputs that can be seamlessly imported in a tier project without extra configuration or processing.

Building the project with rollup.js

We chose rollup.js as bundler tool for our library because it is well adapted: it’s efficient and easy to configure. Other module bundlers like Webpack and Parcel provide advanced features for a developer (dev-server with hot module replacement, for example) but those things are not required for our achievement.

We want the following outputs:

  • React components as ES modules (preferred to CommonJs as it is more future proof and allows tree-shaking)
  • TypeScript declarations files
  • SVGs
  • Source maps
  • Sass files
  • Design tokens

Rollup configuration

Rollup is configured thanks to a rollup.config.js file at the root of our project.

Plugins

Rollup has a bunch of plugins. For our needs we use:

  • rollup-plugin-typescript2 (to transpile TypeScript files and generate declarations)
  • rollup-plugin-json (to convert our token .json files to ES6 modules)
  • rollup-plugin-svgo (to export SVGs through JavaScript)

Note: currently, we don’t use a plugin to convert our Sass files to CSS ones. It’s a deliberate choice as we want to output our design system variables and mixins to use them in other projects using Sass. But it would be great if we could support both Sass and CSS, in order to stick with our “ready-to-use” principle. So we’ve scheduled this task in our roadmap.

Entry point and output

Rollup’s config requires an entry point that will be used to resolve the dependencies

{
  "input": "src/index.ts"
}

This file will export all our components

export { BasicCell } from "./components/BasicCell/"
export { BulletList, BulletListItem } from "./components/BulletList/"
export { Button, ButtonGroup } from "./components/Button/"
...

Then we define how the build result should be output

{
  "output": {
    "file": "dist/index.js",
    "format": "esm",
    "sourcemap": true
  }
}
  • The result will be put in a dist folder
  • We tell Rollup to generate the sourcemaps
  • We tell Rollup to generate ES modules. This will allow Tree Shaking

Exclude external dependencies

Our project is based on React and so uses some external dependencies (react, react-dom, classnames …). We don’t want such libraries to be resolved and bundled with our project. So, all dependencies external to the project (described in package.json dependencies/peerDependencies) are configured to be excluded from the build process

{
    external: [
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {})
    ],
}

TypeScript declarations (types)

Our project being set up with TypeScript, we want to export our declarations files (*.d.ts) so our lib API will be smoothly consumed (available) by any other TypeScript project.

We have 2 options:

  • Put declaration files with the same name and at the same level as your JavaScript modules. So for the hello.js module, TypeScript will look for a hello.d.ts file in the same directory.
  • Declare the entry point for your declaration file in your package.json

Example:

{
  "types": "types/index.d.ts"
}

We recommend the latter. It allows you to separate the types (TypeScript related) from your JavaScript code source

Going further with the rollup configuration

To obtain this result, you can configure the TypeScript compiler (tsconfig/json) to generate the declaration files in a dedicated directory

{
    "compilerOptions": {
        "declaration": true,
        "declarationDir": "dist/types",

Then you can configure rollup to use this setting for its TypeScript plugin

{
    plugins: [
        typescript({
            typescript: require("typescript"),
            useTsconfigDeclarationDir: true
        }),
        ...
}

Build task

We launch rollup to build our project:

yarn rollup -c

The result is available in the dist folder (as defined in the rollup output config)

rollup build result

Note: Our full build script launches rollup then copy our package.json to the dist folder.

Publishing

We are now ready to publish our package to NPM so it will be available as a dependency to another project (using npm install / yarn add). NPM has different mechanisms to specify the files to package and publish:

These 2 approaches are somewhat different, and one is not necessarily preferable to the other. However, the blacklisting strategy tends to be riskier as it exposes files that are sensitive or not relevant. Keep in mind that some files are always included, regardless of settings:

  • package.json
  • README
  • CHANGES / CHANGELOG / HISTORY
  • LICENSE / LICENCE
  • NOTICE
  • The file in the “main” field

In package.json:

{
    "files": [
        "tokens/**/*",
        "**/*.{scss,d.ts,js.map,svg,png,woff,woff2}",
        ".stylelintrc.js"
    ]

Tip: It’s not easy to visualize files that are packaged by NPM (npmjs.com doesn’t list files of a module) To do so with your local project, execute the following command in the directory containing your package.json file:

npm pack && tar -xvzf _.tgz && rm -rf package _.tgz

It will output the files that will be included in the publishing process

Testing/using locally

When importing our components in a tier-project and using them in a real context, we can often encounter conflicts (from tier css rules for example) and unintended behavior, or simply discover some bugs. We can’t afford to wait for our components to be published to encounter those issues. We must have a way to test our components in a tier project before publishing to NPM We can use the npm/yarn link mechanism to symlink our component project and add it to the node_modules of another one. However, such a mechanism doesn’t allow us to perfectly reflect how our project will be packaged by NPM and deployed in another project. Plus, some configuration files that would not be present in the node modules (filtered by the NPM publishing process) can interfere. To be closer to the real process (how our package will be published and installed), we used an advanced tool named yalc.

yalc allows to package a project and add it as a node module like NPM would do, but in a local store.

Conclusion

We can now use our design system assets in any frontend projects. There are natural advantages that come with using a centralised library: single source of truth, reduced maintenance cost, etc. But using a centralised library also enables us to invest more in our design system, which in turn makes it easier for the design system to be adopted company-wide.

Did you enjoy this post? Join Getaround's engineering team!
View openings