├── .gitignore ├── .storybook ├── main.js └── preview.js ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── components │ ├── Button │ │ ├── Button.scss │ │ ├── Button.stories.tsx │ │ ├── Button.test.tsx │ │ ├── Button.tsx │ │ └── index.ts │ └── index.ts ├── index.ts └── stories │ ├── Button.stories.tsx │ ├── Button.tsx │ ├── Header.stories.tsx │ ├── Header.tsx │ ├── Introduction.stories.mdx │ ├── Page.stories.tsx │ ├── Page.tsx │ ├── assets │ ├── code-brackets.svg │ ├── colors.svg │ ├── comments.svg │ ├── direction.svg │ ├── flow.svg │ ├── plugin.svg │ ├── repo.svg │ └── stackalt.svg │ ├── button.css │ ├── header.css │ └── page.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .vscode -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/preset-scss" 10 | ], 11 | "core": { 12 | "builder": "webpack5" 13 | } 14 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Component Library Template 2 | 3 | This project is part of a [blog & video tutorial](https://dev.to/alexeagleson/how-to-create-and-publish-a-react-component-library-2oe) on how to create and publish your own component library. 4 | 5 | [![React Component Library Video Tutorial](https://img.youtube.com/vi/XHQi5a0TmMc/0.jpg)](https://youtu.be/XHQi5a0TmMc) 6 | 7 | You can use this repo to fork as a template for your own React library projects. 8 | 9 | ## Available Scripts 10 | 11 | ### Build the library 12 | 13 | ``` 14 | npm run rollup 15 | ``` 16 | 17 | ### Publish the library 18 | 19 | ``` 20 | npm publish 21 | ``` 22 | 23 | ### Run tests 24 | 25 | ``` 26 | npm run test 27 | ``` 28 | 29 | ### Run storybook locally 30 | 31 | ``` 32 | npm run storybook 33 | ``` 34 | 35 | ### Build storybook 36 | 37 | ``` 38 | npm run build-storybook 39 | ``` 40 | 41 | ## Tutorial 42 | 43 | 1. [Introduction](#introduction) 44 | 1. [Prerequisites and Setup](#prerequisites-and-setup) 45 | 1. [Creating Components](#creating-components) 46 | 1. [Adding Typescript](#adding-typescript) 47 | 1. [Adding Rollup](#adding-rollup) 48 | 1. [Building Your Library](#building-your-library) 49 | 1. [Publishing Your Library](#publishing-your-library) 50 | 1. [Using Your Library](#using-your-library) 51 | 1. [Adding CSS](#adding-css) 52 | 1. [Optimizing](#optimizing) 53 | 1. [Adding Tests](#adding-tests) 54 | 1. [Adding Storybook](#adding-storybook) 55 | 1. [Adding SCSS](#adding-scss) 56 | 1. [Wrapping Up](#wrapping-up) 57 | 58 | ## Introduction 59 | 60 | This tutorial will take you through the process of creating and publishing your own custom React component library and hosting it on Github. 61 | 62 | At the end of this tutorial you will have the ability to the following in all of your future React projects: 63 | 64 | ```bash 65 | npm install @my-github-account/my-cool-component-library 66 | ``` 67 | 68 | ```jsx 69 | import MyCustomComponent from '@my-github-account/my-cool-component-library'; 70 | 71 | const MyApp = () => { 72 | return ( 73 |
74 | 75 |
76 | ) 77 | } 78 | ``` 79 | 80 | ## Prerequisites and Setup 81 | 82 | This project assumes you are familiar with and have installed: 83 | 84 | * Code editor / IDE (this tutorial uses VS Code but any IDE will work) 85 | * NPM (NPM is installed when you install Node.js on your machine) 86 | * Installing packages (presume you know how to add packages to a Javascript project with `npm install`) 87 | * Bash terminal (or another terminal you are comfortable with for running commands) 88 | * Git (we will be creating a git repository on our machine and publishing it to Github, though all instructions will be provided on how to follow along) 89 | * React (how to create simple components using JSX) 90 | * Typescript (how to create an object interface with simple properties) 91 | 92 | First we will initialize our project. 93 | 94 | ```bash 95 | npm init 96 | ``` 97 | 98 | You can take the defaults for all the values, we'll edit them later in the tutorial. 99 | 100 | Next we will add add the tools necessary to create our components. 101 | 102 | ```bash 103 | npm install react typescript @types/react --save-dev 104 | ``` 105 | 106 | ## Creating Components 107 | 108 | Now we can create our first component. Because we are creating a library, we are going to create _index_ files for each tier, and export our components from each one to make it as easy as possible for the people using our library to import them. 109 | 110 | Within the root of your project, create the following file structure: 111 | 112 | ``` 113 | . 114 | ├── src 115 | │ ├── components 116 | | │ ├── Button 117 | | | │ ├── Button.tsx 118 | | | │ └── index.ts 119 | | │ └── index.ts 120 | │ └── index.ts 121 | ├── package.json 122 | └── package-lock.json 123 | ``` 124 | 125 | Make sure to double check your structure. You should have three `index.ts` files, and a `Button.tsx` file inside of a `Button` directory. If you have a preferred way of structuring React components within a project you are of course welcome to do it however you like, but this is the structure we will follow for this tutorial. 126 | 127 | Begin by creating `Button.tsx`: 128 | 129 | `src/components/Button/Button.tsx` 130 | ```tsx 131 | import React from "react"; 132 | 133 | export interface ButtonProps { 134 | label: string; 135 | } 136 | 137 | const Button = (props: ButtonProps) => { 138 | return ; 139 | }; 140 | 141 | export default Button; 142 | ``` 143 | 144 | To keep things simple we will just export a button that takes a single prop called `label`. We can add more complexity and styles to our components once we have confirmed that our basic template is setup correctly. 145 | 146 | After our button, we update the index file inside our Button directory: 147 | 148 | `src/components/Button/index.ts` 149 | ```ts 150 | export { default } from "./Button"; 151 | ``` 152 | 153 | Then we export that button from the components directory: 154 | 155 | `src/components/index.ts` 156 | ```ts 157 | export { default as Button } from "./Button"; 158 | ``` 159 | 160 | And finally, we will export all of our components from the base _src_ directory: 161 | 162 | `src/index.ts` 163 | ```ts 164 | export * from './components'; 165 | ``` 166 | 167 | ## Adding Typescript 168 | 169 | Up until now, we haven't yet initialized Typescript in our project. Although you technically don't need a configuration file to use Typescript, for the complexity of building a library we are definitely going to need one. 170 | 171 | You can initialize a default configuration by running the following command: 172 | 173 | ```bash 174 | npx tsc --init 175 | ``` 176 | 177 | That will create a `tsconfig.json` file for us in the root of our project that contains all the default configuration options for Typescript. 178 | 179 | If you would like to learn more about the many options in a `tsconfig.json` file, modern versions of TS will automatically create descriptive comments for each value. In addition you can find full documentation on the configuration [here](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). 180 | 181 | You may notice depending on your IDE that immediately after initializing you begin to get errors in your project. There are two reasons for that: the first is that Typescript isn't configuration to understand React by default, and the second is that we haven't defined our method for handling modules yet: so it may not understand how to manage all of our exports. 182 | 183 | To fix this we are going to add the following values to `tsconfig.json`: 184 | 185 | ```json 186 | { 187 | "compilerOptions": { 188 | // Default 189 | "target": "es5", 190 | "esModuleInterop": true, 191 | "forceConsistentCasingInFileNames": true, 192 | "strict": true, 193 | "skipLibCheck": true, 194 | 195 | // Added 196 | "jsx": "react", 197 | "module": "ESNext", 198 | "declaration": true, 199 | "declarationDir": "types", 200 | "sourceMap": true, 201 | "outDir": "dist", 202 | "moduleResolution": "node", 203 | "allowSyntheticDefaultImports": true, 204 | "emitDeclarationOnly": true, 205 | } 206 | } 207 | ``` 208 | 209 | I have separated these values into a couple different sections based on the default `tsconfig.json` created using the most recent version of Typescript as of this writing (4.4). The values commented _default_ should already be set for you by default (you will want to double check and make sure however). 210 | 211 | The values marked _added_ are new values that we need for our project. We'll briefly outline why we need them: 212 | 213 | * "jsx": "react" -- Transform JSX into React code 214 | * "module": "ESNext" -- Generate modern JS modules for our library 215 | * "declaration": true -- Output a `.d.ts` file for our library types 216 | * "declarationDir": "types" -- Where to place the `.d.ts` files 217 | * "sourceMap": true -- Mapping JS code back to its TS file origins for debugging 218 | * "outDir": "dist" -- Directory where the project will be generated 219 | * "moduleResolution": "node" -- Follow node.js rules for finding modules 220 | * "allowSyntheticDefaultImports": true -- Assumes default exports if none are created manually 221 | * "emitDeclarationOnly": true -- Don't generate JS (rollup will do that) only export type declarations 222 | 223 | One you add those values to your TS configuration file you should see the errors in `Button.tsx` and other files immediately disappear. 224 | 225 | ## Adding Rollup 226 | 227 | Next we will add [rollup](https://rollupjs.org/guide/en/) to our project. If you've never used rollup before, it's very similar to [webpack](https://webpack.js.org/) in that it is a tool for bundling individual Javascript modules into a single source that a browser is better able to understand. 228 | 229 | Though both tools can accomplish the same goal depending on configuration, typically webpack is used for bundling applications while rollup is particularly suited for bundling libraries (like ours). That's why we've chosen rollup. 230 | 231 | Also similar to webpack, rollup uses a _plugin ecosystem_. By design rollup does not know how to do everything, it relies on plugins installed individually to add the functionality that you need. 232 | 233 | We are going to rely on four plugins for the initial configuration of our library (more will be added later): 234 | 235 | * [@rollup/plugin-node-resolve](https://www.npmjs.com/package/@rollup/plugin-node-resolve) - Uses the [node resolution algorithm](https://nodejs.org/api/modules.html#modules_all_together) for modules 236 | * [@rollup/plugin-typescript](https://www.npmjs.com/package/@rollup/plugin-typescript) - Teaches rollup how to process Typescript files 237 | * [@rollup/plugin-commonjs](https://www.npmjs.com/package/@rollup/plugin-commonjs) - Converts commonjs modules to ES6 modules 238 | * [rollup-plugin-dts](https://www.npmjs.com/package/rollup-plugin-dts) - rollup your `.d.ts` files 239 | 240 | So with that said, let's go ahead and install rollup and our plugins: 241 | 242 | ```bash 243 | npm install rollup @rollup/plugin-node-resolve @rollup/plugin-typescript @rollup/plugin-commonjs rollup-plugin-dts --save-dev 244 | ``` 245 | 246 | To configure how rollup is going to bundle our library we need to create a configuration file in the root of our project: 247 | 248 | `rollup.config.js` 249 | ```js 250 | import resolve from "@rollup/plugin-node-resolve"; 251 | import commonjs from "@rollup/plugin-commonjs"; 252 | import typescript from "@rollup/plugin-typescript"; 253 | import dts from "rollup-plugin-dts"; 254 | 255 | const packageJson = require("./package.json"); 256 | 257 | export default [ 258 | { 259 | input: "src/index.ts", 260 | output: [ 261 | { 262 | file: packageJson.main, 263 | format: "cjs", 264 | sourcemap: true, 265 | }, 266 | { 267 | file: packageJson.module, 268 | format: "esm", 269 | sourcemap: true, 270 | }, 271 | ], 272 | plugins: [ 273 | resolve(), 274 | commonjs(), 275 | typescript({ tsconfig: "./tsconfig.json" }), 276 | ], 277 | }, 278 | { 279 | input: "dist/esm/types/index.d.ts", 280 | output: [{ file: "dist/index.d.ts", format: "esm" }], 281 | plugins: [dts()], 282 | }, 283 | ]; 284 | ``` 285 | 286 | In this file we import our four plugins that we installed. We also import our `package.json` file as a commonJS module int oa variable called `packageJson`. We use this variable to refer to the _main_ and _module_ values that we will define in the next section. 287 | 288 | The entrypoint for our library (input) is the `index.ts` file in the `src` directory which exports all of our components. We will be distributing both ES6 and commonJS modules so the consumers of our library can choose which type work best for them. We also invoke three of our four plugins on the first of two configuration objects on the exported array. This first configuration defines how the actual Javascript code of our library is generated. 289 | 290 | The second configuration object defines how our libraries types are distributed and uses the `dts` plugin to do so. 291 | 292 | The final step before we can run our first rollup is to define the values of "main" and "module" in our `package.json` file: 293 | 294 | `package.json` 295 | ```json 296 | { 297 | "name": "template-react-component-library", 298 | "version": "0.0.1", 299 | "description": "A simple template for a custom React component library", 300 | "scripts": { 301 | "rollup": "rollup -c" 302 | }, 303 | "author": "Alex Eagleson", 304 | "license": "ISC", 305 | "devDependencies": { 306 | "@rollup/plugin-commonjs": "^21.0.1", 307 | "@rollup/plugin-node-resolve": "^13.0.6", 308 | "@rollup/plugin-typescript": "^8.3.0", 309 | "@types/react": "^17.0.34", 310 | "react": "^17.0.2", 311 | "rollup": "^2.60.0", 312 | "rollup-plugin-dts": "^4.0.1", 313 | "typescript": "^4.4.4" 314 | }, 315 | "main": "dist/cjs/index.js", 316 | "module": "dist/esm/index.js", 317 | "files": [ 318 | "dist" 319 | ], 320 | "types": "dist/index.d.ts" 321 | } 322 | ``` 323 | 324 | Here is the sample of the `package.json` file we are using for this tutorial. Obviously your author name can be different, and the specific version of each of your libraries might be different as well. 325 | 326 | The most important changes are as follows: 327 | 328 | * "main" -- We have defined the output path for commonjs modules 329 | * "module" -- We have defined the output path for es6 modules 330 | * "files" -- We have defined the output directory for our entire library 331 | * "types" -- We have defined the location for our library's types 332 | * "scripts" -- We have defined a new script called **rollup**. This will run the rollup package with the -c flag which means "use the rollup configuration file". If you're not familiar with _script_ in a `package.json` file, these are simply shorthand commands you can run by name with `npm run {SCRIPTNAME}`. So to run this one will be `npm run rollup`. 333 | 334 | ## Building your library 335 | 336 | With these configurations in place you are now ready to run rollup for the first time and make sure your basic configuration is correct. Your project structure should look like this before you run: 337 | 338 | ``` 339 | . 340 | ├── src 341 | │ ├── components 342 | | │ ├── Button 343 | | | │ ├── Button.tsx 344 | | | │ └── index.ts 345 | | │ └── index.ts 346 | │ └── index.ts 347 | ├── package.json 348 | ├── package-lock.json 349 | ├── tsconfig.json 350 | └── rollup.config.js 351 | ``` 352 | 353 | The contents of each file should be as described above. Once you have confirmed this, run the following command: 354 | 355 | ```bash 356 | npm run rollup 357 | ``` 358 | 359 | If everything has been configured correctly rollup will run without error and you will see a `dist` directory created in the root of your project with a structure that looks like: 360 | 361 | ![dist directory](https://res.cloudinary.com/dqse2txyi/image/upload/v1637012971/e_dxqcid.png) 362 | 363 | _(If you received an error make sure to read it closely to try and identify the issue. Double check that each of your files follows exactly the structure of the examples. Depending on the amount of time passed since the publishing of this tutorial, new major versions of libraries could potentially be published with breaking changes. All versions of libraries numbers are visible above in the `package.json` example in the event you need to specify a specific version)_ 364 | 365 | ## Publishing your library 366 | 367 | Now that we've created our component library, we need a way to allow ourselves (or others) to download and install it. We will be publishing our library via NPM through hosting on Github. First before anything else we need to create a repository for our library. 368 | 369 | Create a new repository on Github. I have titled mine `template-react-component-library`. Then follow the steps to initialize your project as a git project, and push to your new repository. 370 | 371 | Log into Github and create a new repository called whatever you like. For this example I've titled it `template-react-component-library` and it will be available for everyone to clone and use publicly. You can choose to make your library private if you like, methods described in this tutorial will work for private packages as well (in case you are making a library for your company for example). 372 | 373 | Once the repository is created we need to initialize git within our project locally. Run the following command: 374 | 375 | ``` 376 | git init 377 | ``` 378 | 379 | Next create a `.gitignore` file in the root of the directory (make particular note of the leading period, that signifies this is a hidden file): 380 | 381 | `.gitignore` 382 | ``` 383 | dist 384 | node_modules 385 | ``` 386 | 387 | In our `.gitignore` file we are adding the `dist` and `node_modules` directories. The reason being that both of these are auto-generated directories that we create using commands, so there is no need to include them in our repository. 388 | 389 | Now follow the instructions on Github shown in your new repository for committing your code. 390 | 391 | This repository that you have created is the one you will clone & edit when you want to make changes and updates to your component library. This is not the package itself that your (as a user) would install and use. To configure within our project where our package needs to be published to, next we need to update `package.json` with that information: 392 | 393 | `package.json` 394 | ``` 395 | { 396 | "name": "@YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME", 397 | "publishConfig": { 398 | "registry": "https://npm.pkg.github.com/YOUR_GITHUB_USERNAME" 399 | }, 400 | ... 401 | } 402 | ``` 403 | 404 | You will be updating the field "name" value and adding a new field called "publishConfig". Note the values above in caps are meant to be replaced with your own values. For example my "name" field value would be `@alexeagleson/template-react-component-library`. Notice the "packageConfig" also has your Github account name in it as well, but that value does not lead with the @ symbol. 405 | 406 | Now that we have configured out project, we need to configure our local install of _NPM_ itself to be authorized to publish to your Github account. To do this we use a `.npmrc` file. 407 | 408 | This file is **NOT PART OF OUR PROJECT**. This is a global file in a central location. For Mac/Linux users it goes in your home directory `~/.npmrc`. 409 | 410 | For Windows users it goes in your home directory as well, though the syntax will be different. Something along the lines of `C:\Users\{YOUR_WINDOWS_USERNAME}` 411 | 412 | For more information about this configuration file [read this](https://docs.npmjs.com/cli/v7/configuring-npm/npmrc). 413 | 414 | Once you have created the file, edit it to include the following information: 415 | 416 | `~/.npmrc` 417 | ``` 418 | registry=https://registry.npmjs.org/ 419 | @YOUR_GITHUB_USERNAME:registry=https://npm.pkg.github.com/ 420 | //npm.pkg.github.com/:_authToken=YOUR_AUTH_TOKEN 421 | ``` 422 | 423 | There are two values in caps to replace in the example above. The first is YOUR_GITHUB_USERNAME. Make sure to include the leading @ symbol. 424 | 425 | The second is YOUR_AUTH_TOKEN which we haven't created yet. Back to Github! 426 | 427 | Go to your Github profile: Settings -> Developer Settings -> Personal access tokens. Or just click [this link](https://github.com/settings/tokens) 428 | 429 | 430 | Click _**Generate new token**_. Give it a name that suits the project you are building. Give it an expiry date (Github recommends you don't create tokens with an infinite lifespan for security reasons, but that's up to you). 431 | 432 | The most important thing is to click the `write:packages` access value. This will give your token permission to read & write packages to your Github account, which is wht we need. 433 | 434 | ![Generate new token](https://res.cloudinary.com/dqse2txyi/image/upload/v1637028886/template-react-component-library/accesstoken_hl9kr3.png) 435 | 436 | Once you are done you can click to create the token. Github will **ONLY SHOW YOU THE TOKEN ONCE**. When you close/refresh the page it will be gone, so make sure to copy it to a secure location (perhaps a password manager if you use one). 437 | 438 | The main location you need to place this token is in the `~/.npmrc` file that you created replacing the `YOUR_AUTH_TOKEN` value from the example above. 439 | 440 | Before you continue, do one more sanity check to be sure you didn't create the `.npmrc` file in the root directory of your actual library project. This is technically an option, however the reason you need to be careful is that you could accidentally commit it to your Github repository with the rest of your library code and expose your token to the public. If your `.npmrc` file is in your home directory the risk of this is minimized. 441 | 442 | At this point, once you `~/.npmrc` file has both your Github username and access token added, go back to your project directory and run the following command: 443 | 444 | ```bash 445 | npm publish 446 | ``` 447 | 448 | _(If you get prompted for login credentials, the username is your Github username and your password is the access token you generated)_ 449 | 450 | Congratulations! You have now published version 0.0.1 of your React component library! You can view it on your Github account by going to your main account dashboard and clicking "packages" along the top to the right of "repositories":: 451 | 452 | ![Github packages](https://res.cloudinary.com/dqse2txyi/image/upload/v1637015350/packages_nzibj0.png) 453 | 454 | ## Using Your Library 455 | 456 | Now that your library is live, you'll want to use it! 457 | 458 | Note that the instructions for using your library are slightly different if you published to a _private_ repository. Everyone (aside from your own machine) who tries to import it is going to get a _404 Not Found_ error if they are not authorized. 459 | 460 | Those users also need to add a `~/.npmrc` file with the same information. To be more secure however you can provide those users with an access token that has only **read privileges**, not write. 461 | 462 | _(From this point onward we will presume you have completed that step, or are working with a public repository.)_ 463 | 464 | Since we have created a component library using React and Typescript, we are presuming that the consumers of our library will be using those tools as well. Technically all of our type files `(.d.ts)` are supplemental: meaning they are simply ignored if working with standard Javascript, so it's not necessary to use Typescript to use our library. The types are simply there if desired. 465 | 466 | For our example we will use it however so that we can confirm that they are working properly. We will initialize a React app using one of the most popular and simple methods: [Create React App](https://reactjs.org/docs/create-a-new-react-app.html). 467 | 468 | Run the following command in a **new directory**: 469 | 470 | _(Remember we are simulating other users downloading and installing our library, so this project should be completely separate from the library itself)_ 471 | 472 | ```bash 473 | npx create-react-app my-app --template typescript 474 | ``` 475 | 476 | Open the new `my-app` directory that is created and run: 477 | 478 | ```bash 479 | npm run start 480 | ``` 481 | 482 | Confirm that you are able to open and load the default application screen on `localhost:3000` (or whatever port it opens on). 483 | 484 | Now comes the test for our library. From the root directory of your new `my-app` project, run the following command: 485 | 486 | ```bash 487 | npm install @YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME 488 | ``` 489 | 490 | So for my project for example its: `npm install @alexeagleson/template-react-component-library` 491 | 492 | Presuming your tokens and configuration are set up properly, everything will install correctly _(if there are any issues, revisit the example for the `~/.npmrc` config.)_ 493 | 494 | Now open the `my-app` project in your IDE of choice (VS Code for example) and navigate to the `src/App.tsx` file. 495 | 496 | When you go to add a `; 552 | }; 553 | 554 | export default Button; 555 | ``` 556 | 557 | Notice the `import './Button.css'` that has been added. 558 | 559 | Now we need to tell rollup how to process that syntax. To do that we use a plugin called `rollup-plugin-postcss`. Run the following command: 560 | 561 | ```bash 562 | npm install rollup-plugin-postcss --save-dev 563 | ``` 564 | 565 | Next we need to update our rollup config: 566 | 567 | `rollup.config.js` 568 | ```js 569 | import resolve from "@rollup/plugin-node-resolve"; 570 | import commonjs from "@rollup/plugin-commonjs"; 571 | import typescript from "@rollup/plugin-typescript"; 572 | import dts from "rollup-plugin-dts"; 573 | 574 | // NEW 575 | import postcss from "rollup-plugin-postcss"; 576 | 577 | const packageJson = require("./package.json"); 578 | 579 | export default [ 580 | { 581 | input: "src/index.ts", 582 | output: [ 583 | { 584 | file: packageJson.main, 585 | format: "cjs", 586 | sourcemap: true, 587 | }, 588 | { 589 | file: packageJson.module, 590 | format: "esm", 591 | sourcemap: true, 592 | }, 593 | ], 594 | plugins: [ 595 | resolve(), 596 | commonjs(), 597 | typescript({ tsconfig: "./tsconfig.json" }), 598 | 599 | // NEW 600 | postcss(), 601 | ], 602 | }, 603 | { 604 | input: "dist/esm/types/index.d.ts", 605 | output: [{ file: "dist/index.d.ts", format: "esm" }], 606 | plugins: [dts()], 607 | 608 | // NEW 609 | external: [/\.(css|less|scss)$/], 610 | }, 611 | ]; 612 | 613 | ``` 614 | 615 | Note the three new lines indicated with the `NEW` comments. In the `dts` config we need to specify that `.css` modules are external and should not be processed as part of our type definitions (otherwise we will get an error). 616 | 617 | Finally we need to update the _version number_ in our `package.json` file. Remember we are publishing a package so when we make changes, we need to ensure we don't impact users of previous versions of our library. Every time we publish we should increment the version number: 618 | 619 | `package.json` 620 | ```json 621 | { 622 | "version": "0.0.2", 623 | ... 624 | } 625 | ``` 626 | 627 | Now run these commands: 628 | 629 | ```bash 630 | npm run rollup 631 | npm publish 632 | ``` 633 | 634 | On the library consuming side (`my-app` React app from our tutorial) we also need to update to get the latest version of the package. The easiest way is to increment the version number in the `package.json` file of `my-app`. It should show `^0.0.1`. Increment that to `^0.0.2` and then you can update with the `npm install` command: 635 | 636 | ```bash 637 | npm install 638 | npm run start 639 | ``` 640 | 641 | And you'll be treated to a giant button component from our library that now supports bundling CSS! 642 | 643 | ![Large Button](https://res.cloudinary.com/dqse2txyi/image/upload/v1637028364/template-react-component-library/helloworldbuttonbig_lyapwq.png) 644 | 645 | 646 | ## Optimizing 647 | 648 | There are a couple of easy optimizations we can make with this setup. The first is to add a plugin called [terser](https://www.npmjs.com/package/rollup-plugin-terser) that will minify our bundle and reduce the overall file size. 649 | 650 | The other is to update some of our dependencies to `peerDependencies`. With rollup's [peer dependencies](https://www.npmjs.com/package/rollup-plugin-peer-deps-external) plugin we can tell the projects that are using our libraries which dependencies are required (like React) but won't actually bundle a copy of React with the library itself. If the consumer already has React in their project it will use that, otherwise it will get installed when they run `npm install`. 651 | 652 | First we will install these two plugins: 653 | 654 | ```bash 655 | npm install rollup-plugin-peer-deps-external rollup-plugin-terser --save-dev 656 | ``` 657 | 658 | Then we will update our rollup config: 659 | 660 | `rollup.config.js` 661 | ```js 662 | import resolve from "@rollup/plugin-node-resolve"; 663 | import commonjs from "@rollup/plugin-commonjs"; 664 | import typescript from "@rollup/plugin-typescript"; 665 | import postcss from "rollup-plugin-postcss"; 666 | import dts from "rollup-plugin-dts"; 667 | 668 | //NEW 669 | import { terser } from "rollup-plugin-terser"; 670 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 671 | 672 | const packageJson = require("./package.json"); 673 | 674 | export default [ 675 | { 676 | input: "src/index.ts", 677 | output: [ 678 | { 679 | file: packageJson.main, 680 | format: "cjs", 681 | sourcemap: true, 682 | }, 683 | { 684 | file: packageJson.module, 685 | format: "esm", 686 | sourcemap: true, 687 | }, 688 | ], 689 | plugins: [ 690 | // NEW 691 | peerDepsExternal(), 692 | 693 | resolve(), 694 | commonjs(), 695 | typescript({ tsconfig: "./tsconfig.json" }), 696 | postcss(), 697 | 698 | // NEW 699 | terser(), 700 | ], 701 | }, 702 | { 703 | input: "dist/esm/types/index.d.ts", 704 | output: [{ file: "dist/index.d.ts", format: "esm" }], 705 | plugins: [dts()], 706 | external: [/\.css$/], 707 | }, 708 | ]; 709 | ``` 710 | 711 | Then we move React from `devDependencies` to `peerDependencies` in our `package.json` file: 712 | 713 | `package.json` 714 | ```json 715 | { 716 | "devDependencies": { 717 | "@rollup/plugin-commonjs": "^21.0.1", 718 | "@rollup/plugin-node-resolve": "^13.0.6", 719 | "@rollup/plugin-typescript": "^8.3.0", 720 | "@types/react": "^17.0.34", 721 | "rollup": "^2.60.0", 722 | "rollup-plugin-dts": "^4.0.1", 723 | "rollup-plugin-peer-deps-external": "^2.2.4", 724 | "rollup-plugin-postcss": "^4.0.1", 725 | "rollup-plugin-terser": "^7.0.2", 726 | "typescript": "^4.4.4" 727 | }, 728 | "peerDependencies": { 729 | "react": "^17.0.2" 730 | }, 731 | ... 732 | ``` 733 | 734 | ## Adding Tests 735 | 736 | To add tests for our components we are going to install [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), and to run those tests we will install [Jest](https://jestjs.io/). 737 | 738 | ```bash 739 | npm install @testing-library/react jest @types/jest --save-dev 740 | ``` 741 | 742 | Inside of our Button directory, create a new file called `Button.test.tsx` 743 | 744 | `src/components/Button/Button.test.tsx` 745 | ```tsx 746 | import React from "react"; 747 | import { render } from "@testing-library/react"; 748 | 749 | import Button from "./Button"; 750 | 751 | describe("Button", () => { 752 | test("renders the Button component", () => { 753 | render(; 10 | }; 11 | 12 | export default Button; 13 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Button"; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from "./Button"; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; -------------------------------------------------------------------------------- /src/stories/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Button } from './Button'; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | export default { 8 | title: 'Example/Button', 9 | component: Button, 10 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | } as ComponentMeta; 15 | 16 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 17 | const Template: ComponentStory = (args) => 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/stories/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Header } from './Header'; 5 | 6 | export default { 7 | title: 'Example/Header', 8 | component: Header, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) =>
; 12 | 13 | export const LoggedIn = Template.bind({}); 14 | LoggedIn.args = { 15 | user: {}, 16 | }; 17 | 18 | export const LoggedOut = Template.bind({}); 19 | LoggedOut.args = {}; 20 | -------------------------------------------------------------------------------- /src/stories/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button } from './Button'; 4 | import './header.css'; 5 | 6 | interface HeaderProps { 7 | user?: {}; 8 | onLogin: () => void; 9 | onLogout: () => void; 10 | onCreateAccount: () => void; 11 | } 12 | 13 | export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( 14 |
15 |
16 |
17 | 18 | 19 | 23 | 27 | 31 | 32 | 33 |

Acme

34 |
35 |
36 | {user ? ( 37 |
45 |
46 |
47 | ); 48 | -------------------------------------------------------------------------------- /src/stories/Introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | import Code from './assets/code-brackets.svg'; 3 | import Colors from './assets/colors.svg'; 4 | import Comments from './assets/comments.svg'; 5 | import Direction from './assets/direction.svg'; 6 | import Flow from './assets/flow.svg'; 7 | import Plugin from './assets/plugin.svg'; 8 | import Repo from './assets/repo.svg'; 9 | import StackAlt from './assets/stackalt.svg'; 10 | 11 | 12 | 13 | 116 | 117 | # Welcome to Storybook 118 | 119 | Storybook helps you build UI components in isolation from your app's business logic, data, and context. 120 | That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. 121 | 122 | Browse example stories now by navigating to them in the sidebar. 123 | View their code in the `src/stories` directory to learn how they work. 124 | We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. 125 | 126 |
Configure
127 | 128 | 174 | 175 |
Learn
176 | 177 | 207 | 208 |
209 | TipEdit the Markdown in{' '} 210 | src/stories/Introduction.stories.mdx 211 |
212 | -------------------------------------------------------------------------------- /src/stories/Page.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | 4 | import { Page } from './Page'; 5 | import * as HeaderStories from './Header.stories'; 6 | 7 | export default { 8 | title: 'Example/Page', 9 | component: Page, 10 | } as ComponentMeta; 11 | 12 | const Template: ComponentStory = (args) => ; 13 | 14 | export const LoggedIn = Template.bind({}); 15 | LoggedIn.args = { 16 | // More on composing args: https://storybook.js.org/docs/react/writing-stories/args#args-composition 17 | ...HeaderStories.LoggedIn.args, 18 | }; 19 | 20 | export const LoggedOut = Template.bind({}); 21 | LoggedOut.args = { 22 | ...HeaderStories.LoggedOut.args, 23 | }; 24 | -------------------------------------------------------------------------------- /src/stories/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Header } from './Header'; 4 | import './page.css'; 5 | 6 | interface PageProps { 7 | user?: {}; 8 | onLogin: () => void; 9 | onLogout: () => void; 10 | onCreateAccount: () => void; 11 | } 12 | 13 | export const Page = ({ user, onLogin, onLogout, onCreateAccount }: PageProps) => ( 14 |
15 |
16 | 17 |
18 |

Pages in Storybook

19 |

20 | We recommend building UIs with a{' '} 21 | 22 | component-driven 23 | {' '} 24 | process starting with atomic components and ending with pages. 25 |

26 |

27 | Render pages with mock data. This makes it easy to build and review page states without 28 | needing to navigate to them in your app. Here are some handy patterns for managing page data 29 | in Storybook: 30 |

31 |
    32 |
  • 33 | Use a higher-level connected component. Storybook helps you compose such data from the 34 | "args" of child component stories 35 |
  • 36 |
  • 37 | Assemble data in the page component from your services. You can mock these services out 38 | using Storybook. 39 |
  • 40 |
41 |

42 | Get a guided tutorial on component-driven development at{' '} 43 | 44 | Storybook tutorials 45 | 46 | . Read more in the{' '} 47 | 48 | docs 49 | 50 | . 51 |

52 |
53 | Tip Adjust the width of the canvas with the{' '} 54 | 55 | 56 | 61 | 62 | 63 | Viewports addon in the toolbar 64 |
65 |
66 |
67 | ); 68 | -------------------------------------------------------------------------------- /src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /src/stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /src/stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 900; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | -------------------------------------------------------------------------------- /src/stories/page.css: -------------------------------------------------------------------------------- 1 | section { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 24px; 5 | padding: 48px 20px; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | color: #333; 9 | } 10 | 11 | h2 { 12 | font-weight: 900; 13 | font-size: 32px; 14 | line-height: 1; 15 | margin: 0 0 4px; 16 | display: inline-block; 17 | vertical-align: top; 18 | } 19 | 20 | p { 21 | margin: 1em 0; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | color: #1ea7fd; 27 | } 28 | 29 | ul { 30 | padding-left: 30px; 31 | margin: 1em 0; 32 | } 33 | 34 | li { 35 | margin-bottom: 8px; 36 | } 37 | 38 | .tip { 39 | display: inline-block; 40 | border-radius: 1em; 41 | font-size: 11px; 42 | line-height: 12px; 43 | font-weight: 700; 44 | background: #e7fdd8; 45 | color: #66bf3c; 46 | padding: 4px 12px; 47 | margin-right: 10px; 48 | vertical-align: top; 49 | } 50 | 51 | .tip-wrapper { 52 | font-size: 13px; 53 | line-height: 20px; 54 | margin-top: 40px; 55 | margin-bottom: 40px; 56 | } 57 | 58 | .tip-wrapper svg { 59 | display: inline-block; 60 | height: 12px; 61 | width: 12px; 62 | margin-right: 4px; 63 | vertical-align: top; 64 | margin-top: 3px; 65 | } 66 | 67 | .tip-wrapper svg path { 68 | fill: #1ea7fd; 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Default 4 | "target": "es5", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | 10 | // Added 11 | "jsx": "react", 12 | "module": "ESNext", 13 | "declaration": true, 14 | "declarationDir": "types", 15 | "sourceMap": true, 16 | "outDir": "dist", 17 | "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "emitDeclarationOnly": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------