The purpose of this series is to walk through creating a small component library so you can learn how to build your own.
This is Part 1 of this series. This section will mostly focus on setting up our module and file structure. But we’ll end by building an example component! 🎉
If you’re looking for Part 2, look here!
I’m not expecting you’ve published a module to npm before, but we’ll start with a few basic assumptions:
It would also be helpful to be familiar with styled-components and Babel, but it’s not required.
First we’ll create a new directory. I’m going to name mine: component-lib.
$ mkdir component-lib
Now we’ll setup Git and GitHub.
$ cd component-lib
$ git init
NOTE: You’ll need to add a repo on GitHub first.
git remote add origin [email protected]:alanbsmith/component-lib.git
Cool. Now we’re ready to setup npm. If you already have an account on npm, you can just run npm login.
Otherwise we’ll need to set up an account for you. We’ll set an author name, email (which is public), and url.
$ npm set init.author.name "Your Name"
$ npm set init.author.email "[email protected]"
$ npm set init.author.url "http://yourblog.com"
$ npm adduser
Now we can run npm init and set up our project accordingly. Most of the default settings are fine for now. The few that you might pay attention to are the version, description, and entry point. And remember you can update all of these later.
$ npm init
name: (component-lib)
version: (1.0.0) 0.1.0
description: an example component library built with React!
entry point: (index.js) build/index.js
test command:
git repository:
keywords:
license: (ISC)
About to write to /Users/alanbsmith/personal-projects/trash/package.json:
{
"name": "component-lib",
"version": "0.1.0",
"description": "an example component library built with React!",
"main": "build/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Alan Smith <[email protected]> (https://github.com/alanbsmith)",
"license": "ISC"
}
Is this ok? (yes)
I typically like to version projects initially at 0.1.0 instead of 1.0.0, which is the default. I also like to add a brief description of what this module is or does. We also need to change the entry point to build/index.js (We’ll explain this more later). Notice that your author and GitHub information was added automatically. This helps connect your npm docs with GitHub, and makes it really easy for your users to learn more about your module.
Awesome. Now we’re ready to add our files and directories.
This section is pretty dry, so I’ll be terse for most of it and touch down for the important bits.
$ mkdir lib
$ touch .babelrc .eslintrc .gitignore .npmignore CODE_OF_CONDUCT.md README.md
$ touch lib/index.js
$ mkdir lib/components lib/elements lib/styles
Great. Now you have the basic file structure you’ll need. A few points:
We’ll get there. I promise. But I didn’t want to cram test setup into this section as well.
This section is also pretty dry. I’ll point out the important parts as we go and skip the rest. I’m using $ npm install for these commands, but if you prefer yarn, go for it.
🚨 The--save-dev at the end is really important! 🚨
We don’t need our users to haul all of this down with our module just to use our compoents. These libs are only used for our local development.
$ npm install babel-cli babel-core babel-eslint babel-preset-es2015 babel-preset-react eslint eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-watch polished prop-types react react-dom styled-components --save-dev
And now we wait…
Whew. That was a lot. Let’s hit the high points. If you’re familar with React, things like react, react-dom, babel (and the presets), and prop-types will look pretty familiar. And if you’re familiar with eslint, the plugins probably look familiar as well.
The potential newcomers are styled-components and polished. As mentioned earlier, we’ll be using the styled-components lib to create our library. And you can think of Polished as a little add-on that gives us some nice Sass functionality. It’s not technically essential, but I thought it would be cool to introduce it here.
Cool. Now that we have that setup, we’ll fill in the little files we created along the way.
A Quick Note:
Below I’m going to use the term “transpile” a bit. The word is a mashup between “transform” and “compile.” If that’s unfamiliar to you, you can mostly equate it with “compile” and don’t worry too much about the details. If you want to know more though, you can read a great article here.
.babelrc
{
"presets": ["es2015", "react"]
}
.eslintrc
People have lots of opinions about their linters. If you want to use your own, go for it. If you don’t have strong feelings, here’s a nice one.
{
root: true,
parser: 'babel-eslint',
plugins: [/*'import', */'jsx-a11y', 'react'],
env: {
browser: true,
commonjs: true,
es6: true,
jest: true,
node: true
},
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
generators: true,
experimentalObjectRestSpread: true
}
},
settings: {
'import/ignore': [
'node_modules',
'\\.(json|css|jpg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm)$',
],
'import/extensions': ['.js'],
'import/resolver': {
node: {
extensions: ['.js', '.json']
}
}
},
rules: {
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
'camelcase': 'warn',
'curly': 'warn',
'default-case': ['warn', { commentPattern: '^no default$' }],
'dot-location': ['warn', 'property'],
'eol-last': 'warn',
'eqeqeq': ['warn', 'always'],
'indent': ['warn', 2, { "SwitchCase": 1 }],
'guard-for-in': 'warn',
'keyword-spacing': 'warn',
'new-parens': 'warn',
'no-array-constructor': 'warn',
'no-caller': 'warn',
'no-cond-assign': ['warn', 'always'],
'no-const-assign': 'warn',
'no-control-regex': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-class-members': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-eval': 'warn',
'no-ex-assign': 'warn',
'no-extend-native': 'warn',
'no-extra-bind': 'warn',
'no-extra-label': 'warn',
'no-fallthrough': 'warn',
'no-func-assign': 'warn',
'no-global-assign': 'warn',
'no-implied-eval': 'warn',
'no-invalid-regexp': 'warn',
'no-iterator': 'warn',
'no-label-var': 'warn',
'no-labels': ['warn', { allowLoop: false, allowSwitch: false }],
'no-lone-blocks': 'warn',
'no-loop-func': 'warn',
'no-mixed-operators': ['warn', {
groups: [
['&', '|', '^', '~', '<<', '>>', '>>>'],
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
['&&', '||'],
['in', 'instanceof']
],
allowSamePrecedence: false
}],
'no-multi-str': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-redeclare': 'warn',
'no-regex-spaces': 'warn',
'no-restricted-syntax': [
'warn',
'LabeledStatement',
'WithStatement',
],
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-sequences': 'warn',
'no-shadow-restricted-names': 'warn',
'no-sparse-arrays': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'warn',
'no-throw-literal': 'warn',
'no-undef': 'warn',
'no-unexpected-multiline': 'warn',
'no-unreachable': 'warn',
'no-unsafe-negation': 'warn',
'no-unused-expressions': 'warn',
'no-unused-labels': 'warn',
'no-unused-vars': ['warn', { vars: 'local', args: 'none' }],
'no-use-before-define': ['warn', 'nofunc'],
'no-useless-computed-key': 'warn',
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-escape': 'warn',
'no-useless-rename': ['warn', {
ignoreDestructuring: false,
ignoreImport: false,
ignoreExport: false,
}],
'no-with': 'warn',
'no-whitespace-before-property': 'warn',
'object-curly-spacing': ['warn', 'always'],
'operator-assignment': ['warn', 'always'],
radix: 'warn',
'require-yield': 'warn',
'rest-spread-spacing': ['warn', 'never'],
'semi': 'warn',
strict: ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': 'warn',
'valid-typeof': 'warn',
'react/jsx-boolean-value': 'warn',
'react/jsx-closing-bracket-location': 'warn',
'react/jsx-curly-spacing': 'warn',
'react/jsx-equals-spacing': ['warn', 'never'],
'react/jsx-first-prop-new-line': ['warn', 'multiline'],
'react/jsx-handler-names': 'warn',
'react/jsx-indent': ['warn', 2],
'react/jsx-indent-props': ['warn', 2],
'react/jsx-key': 'warn',
'react/jsx-max-props-per-line': 'warn',
'react/jsx-no-bind': ['warn', {'allowArrowFunctions': true}],
'react/jsx-no-comment-textnodes': 'warn',
'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }],
'react/jsx-no-undef': 'warn',
'react/jsx-pascal-case': ['warn', {
allowAllCaps: true,
ignore: [],
}],
'react/jsx-sort-props': 'warn',
'react/jsx-tag-spacing': 'warn',
'react/jsx-uses-react': 'warn',
'react/jsx-uses-vars': 'warn',
'react/jsx-wrap-multilines': 'warn',
'react/no-deprecated': 'warn',
'react/no-did-mount-set-state': 'warn',
'react/no-did-update-set-state': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-is-mounted': 'warn',
'react/no-unused-prop-types': 'warn',
'react/prefer-es6-class': 'warn',
'react/prefer-stateless-function': 'warn',
'react/prop-types': 'warn',
'react/react-in-jsx-scope': 'warn',
'react/require-render-return': 'warn',
'react/self-closing-comp': 'warn',
'react/sort-comp': 'warn',
'react/sort-prop-types': 'warn',
'react/style-prop-object': 'warn',
'react/void-dom-elements-no-children': 'warn',
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
'jsx-a11y/aria-role': 'warn',
'jsx-a11y/img-has-alt': 'warn',
'jsx-a11y/img-redundant-alt': 'warn',
'jsx-a11y/no-access-key': 'warn'
}
}
.gitignore
Note that we’re ignoring the build directory. This is where Babel will put all of our transpiled components, and we don’t want to push up (or pull down) double the code if we don’t have to.
.DS_Store
build
node_modules
*.log
.npmignore
Note that we’re ignoring the lib directory. Our users will only interact with the transpiled code in build, so we don’t need (or want) to bulk up their node_modules directory with unnecessary code.
.babelrc
lib
CODE_OF_CONDUCT.md
CODE_OF_CONDUCT.md
You might already have a favorite COC. If so, feel free to use it. If not, the Contributor Covenant Code of Conduct is a great one to use.
README.md
Feel free to add whatever you’d like in your README. Some suggested content would be:
Awesome. Now that all of that is settled, we can add some scripts to our package.json
Inside of our package.json we’ll add these commands to the scripts section:
"scripts": {
"build": "babel lib -d build",
"lint": "eslint lib/**; exit 0",
"lint:watch": "esw -w lib/**",
"prepublish": "npm run build"
},
Cool. Now that we’re done with the basic setup, we can add our first component!
Ok. You’ve made it this far. Now it’s time to do something a little more fun. Let’s create a file called Button.js in lib/elements.
$ touch lib/elements/Button.js
Cool. Inside that file, we’ll add this:
import styled from 'styled-components';
const Button = styled.button`
background: #1FB6FF;
border: none;
border-radius: 2px;
color: #FFFFFF;
cursor: pointer;
display: inline-block;
font-size: 16px;
line-height: 40px;
font-weight: 200;
margin: 8px 0;
outline: none;
padding: 0 12px;
text-transform: uppercase;
transition: all 300ms ease;
&:hover {
background: #009EEB;
}
`;
export default Button;
Now that we have this setup, we need to add it to lib/index.js so our module knows how to find it.
import Button from './elements/Button’;
module.exports = {
Button,
};
Yay! Our little button component is ready to be published. But before we do that, it would be really nice to have a local test environment to experiment with before we publish to npm. And in Part 2, that’s exactly what we’ll discuss! We’ll also talk about component design and dive into styled-components to make this more customizable.
NOTE: This would be a great place to save your work and commit.
$ git status
$ git add -A
$ git commit -m 'Initial commit | adds basic setup'
I hope this was helpful. If you enjoyed reading, let me know! And if you think it would end helpful for others, feel free to share!
Ready for more? Head to Part 2!