19
loading...
This website collects cookies to deliver better user experience
npm install @my-github-account/my-cool-component-library
import MyCustomComponent from '@my-github-account/my-cool-component-library';
const MyApp = () => {
return (
<div>
<MyCustomComponent />
</div>
)
}
npm install
)npm init
npm install react typescript @types/react --save-dev
.
├── src
│ ├── components
| │ ├── Button
| | │ ├── Button.tsx
| | │ └── index.ts
| │ └── index.ts
│ └── index.ts
├── package.json
└── package-lock.json
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. Button.tsx
:src/components/Button/Button.tsx
import React from "react";
export interface ButtonProps {
label: string;
}
const Button = (props: ButtonProps) => {
return <button>{props.label}</button>;
};
export default Button;
label
. We can add more complexity and styles to our components once we have confirmed that our basic template is setup correctly.src/components/Button/index.ts
export { default } from "./Button";
src/components/index.ts
export { default as Button } from "./Button";
src/index.ts
export * from './components';
npx tsc --init
tsconfig.json
file for us in the root of our project that contains all the default configuration options for Typescript. 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.tsconfig.json
:{
"compilerOptions": {
// Default
"target": "es5",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
// Added
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"emitDeclarationOnly": true,
}
}
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)..d.ts
file for our library types.d.ts
filesButton.tsx
and other files immediately disappear..d.ts
filesnpm install rollup @rollup/plugin-node-resolve @rollup/plugin-typescript @rollup/plugin-commonjs rollup-plugin-dts --save-dev
rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
const packageJson = require("./package.json");
export default [
{
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true,
},
{
file: packageJson.module,
format: "esm",
sourcemap: true,
},
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: "./tsconfig.json" }),
],
},
{
input: "dist/esm/types/index.d.ts",
output: [{ file: "dist/index.d.ts", format: "esm" }],
plugins: [dts()],
},
];
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.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.dts
plugin to do so. package.json
file:package.json
{
"name": "template-react-component-library",
"version": "0.0.1",
"description": "A simple template for a custom React component library",
"scripts": {
"rollup": "rollup -c"
},
"author": "Alex Eagleson",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-typescript": "^8.3.0",
"@types/react": "^17.0.34",
"react": "^17.0.2",
"rollup": "^2.60.0",
"rollup-plugin-dts": "^4.0.1",
"typescript": "^4.4.4"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"files": [
"dist"
],
"types": "dist/index.d.ts"
}
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.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
..
├── src
│ ├── components
| │ ├── Button
| | │ ├── Button.tsx
| | │ └── index.ts
| │ └── index.ts
│ └── index.ts
├── package.json
├── package-lock.json
├── tsconfig.json
└── rollup.config.js
npm run rollup
dist
directory created in the root of your project with a structure that looks like:package.json
example in the event you need to specify a specific version)template-react-component-library
. Then follow the steps to initialize your project as a git project, and push to your new repository.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).git init
.gitignore
file in the root of the directory (make particular note of the leading period, that signifies this is a hidden file):.gitignore
dist
node_modules
.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.package.json
with that information:package.json
{
"name": "@YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME",
"publishConfig": {
"registry": "https://npm.pkg.github.com/YOUR_GITHUB_USERNAME"
},
...
}
@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. .npmrc
file. ~/.npmrc
. C:\Users\{YOUR_WINDOWS_USERNAME}
~/.npmrc
registry=https://registry.npmjs.org/
@YOUR_GITHUB_USERNAME:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:_authToken=YOUR_AUTH_TOKEN
write:packages
access value. This will give your token permission to read & write packages to your Github account, which is wht we need. ~/.npmrc
file that you created replacing the YOUR_AUTH_TOKEN
value from the example above..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.~/.npmrc
file has both your Github username and access token added, go back to your project directory and run the following command:npm publish
~/.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. (.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. npx create-react-app my-app --template typescript
my-app
directory that is created and run:npm run start
localhost:3000
(or whatever port it opens on).my-app
project, run the following command:npm install @YOUR_GITHUB_USERNAME/YOUR_REPOSITORY_NAME
npm install @alexeagleson/template-react-component-library
~/.npmrc
config.)my-app
project in your IDE of choice (VS Code for example) and navigate to the src/App.tsx
file. <Button />
component, if your editor supports import auto complete (ctrl/cmd + .
for VS Code) then you will see it automatically recognize thanks to Typescript that our library exports that button. src/App.tsx
is:src/App.tsx
import React from "react";
import { Button } from "@alexeagleson/template-react-component-library";
function App() {
return <Button label="Hello world!"/>;
}
export default App;
npm run start
again, there tucked up in the corner is our Hello world! button. Button
directory where our component lives, we'll create a file called: Button.css
:src/components/Button/Button.css
button {
font-size: 60px;
}
Button.tsx
file with the following:src/components/Button/Button.tsx
import React from "react";
import "./Button.css";
export interface ButtonProps {
label: string;
}
const Button = (props: ButtonProps) => {
return <button>{props.label}</button>;
};
export default Button;
import './Button.css'
that has been added.rollup-plugin-postcss
. Run the following command:npm install rollup-plugin-postcss --save-dev
rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
// NEW
import postcss from "rollup-plugin-postcss";
const packageJson = require("./package.json");
export default [
{
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true,
},
{
file: packageJson.module,
format: "esm",
sourcemap: true,
},
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: "./tsconfig.json" }),
// NEW
postcss(),
],
},
{
input: "dist/esm/types/index.d.ts",
output: [{ file: "dist/index.d.ts", format: "esm" }],
plugins: [dts()],
// NEW
external: [/\.css$/],
},
];
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).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:package.json
{
"version": "0.0.2",
...
}
npm run rollup
npm publish
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:npm install
npm run start
peerDependencies
. With rollup's peer dependencies 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
.npm install rollup-plugin-peer-deps-external rollup-plugin-terser --save-dev
rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import postcss from "rollup-plugin-postcss";
import dts from "rollup-plugin-dts";
//NEW
import { terser } from "rollup-plugin-terser";
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
const packageJson = require("./package.json");
export default [
{
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true,
},
{
file: packageJson.module,
format: "esm",
sourcemap: true,
},
],
plugins: [
// NEW
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ tsconfig: "./tsconfig.json" }),
postcss(),
// NEW
terser(),
],
},
{
input: "dist/esm/types/index.d.ts",
output: [{ file: "dist/index.d.ts", format: "esm" }],
plugins: [dts()],
external: [/\.css$/],
},
];
devDependencies
to peerDependencies
in our package.json
file:package.json
{
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-typescript": "^8.3.0",
"@types/react": "^17.0.34",
"rollup": "^2.60.0",
"rollup-plugin-dts": "^4.0.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.1",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.4.4"
},
"peerDependencies": {
"react": "^17.0.2"
},
...
npm install @testing-library/react jest @types/jest --save-dev
Button.test.tsx
src/components/Button/Button.test.tsx
import React from "react";
import { render } from "@testing-library/react";
import Button from "./Button";
describe("Button", () => {
test("renders the Button component", () => {
render(<Button label="Hello world!" />);
});
});
package.json
. We'll start with the configuration, create a jest.config.js
file in the root of the project:jest.config.js
module.exports = {
testEnvironment: "jsdom",
};
package.json
file:package.json
{
"scripts": {
"rollup": "rollup -c",
"test": "jest"
},
...
}
npm run test
babel-jest
that tells Jest to use Babel! Let's install them now, along with Babel plugins to handle our Typescript and React code. The total collection of all of them looks like:npm install @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript babel-jest --save-dev
babel.config.js
module.exports = {
presets: [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
],
};
npm run test
... but... there is one more problem!import
of the .css
file isn't understood. That makes sense because, again, we configured a postcss
plugin for rollup to handle that, but we did no such thing for Jest.npm install identity-obj-proxy --save-dev
moduleNameMapper
property. We've also added less
and scss
in there for good measure in case you want to expand your project later to use those:jest.config.js
module.exports = {
testEnvironment: "jsdom",
moduleNameMapper: {
".(css|less|scss)$": "identity-obj-proxy",
},
};
npm run test
npx sb init --builder webpack5
scripts
to run it into your package.json
file automatically.stories
directory in your src
directory. This directory is full of pre-built templates for you to use as an example of how to create your own stories. I recommend you don't delete these until you become familiar with Storybook and how to write your own stories, having them close by will be very handy.Button
directory called Button.stories.tsx
:src/components/Button/Button.stories.tsx
import React from "react";
import { ComponentStory, ComponentMeta } from "@storybook/react";
import Button from "./Button";
// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
title: "ReactComponentLibrary/Button",
component: Button,
} as ComponentMeta<typeof Button>;
// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
export const HelloWorld = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
HelloWorld.args = {
label: "Hello world!",
};
export const ClickMe = Template.bind({});
ClickMe.args = {
label: "Click me!",
};
The default export defines where the button will appear in the Storybook. I've chosen ReactComponentLibrary as a simple name to group our custom components together separately from the examples.
The Template determines which component is actually being rendered, and which default args/props to apply to it.
The Template.bind objects are instances or example states of the component. So in a real project you might have something like "LargeButton" and "SmallButton". Since our button is always big I've just used an example of testing the button with two different labels.
package.json
file you'll see that Storybook has already added a storybook
and storybook-build
script. The first will host the Storybook application locally for quick and easy testing. The second one will build a static HTML/JS bundle that can easily be hosted on a remote server, so all members of your team can try your components. npm run storybook
react-dom
. This is not ideal as your project itself should not depend on these libraries, so it should not be necessary to include them as they are included with Storybook's peer dependencies, as example here.npm install
command it will install all the peerDependencies
of the libraries you are using. Before running this you may need to delete your package-lock.json
and node_modules
directory. They will be regenerated automatically after your fresh install. rollup-plugin-postcss
you should already be able to simply rename your .css
file to .scss
and then import 'Button.scss
and be on your way. Running num run rollup
will compile it all just fine with the current configuration.--builder webpack5
flag when installing in the previous section, you will likely encounter a lot of errors trying to configure Storybook to support SCSS with webpack 4. With version 5 it's fairly simple using the SCSS preset.package.json
file. Next delete your package-lock.json
and /node_modules/
directory and initialize Storybook again with the --builder webpack5
flag).npm install @storybook/preset-scss css-loader sass sass-loader style-loader --save-dev
@storybook/preset-scss
to your main Storybook config:.storybook/main.js
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/preset-scss"
],
"core": {
"builder": "webpack5"
}
}
npm run storybook
and see all your SCSS styles.package-lock.json
and node_modules
first and then running npm install
again. This will often fix your issue without requiring you to add unnecessary dependencies to your own project.)