Warning: It's going to be a long read but the effort shall surely be worth the time is something I can guarantee!!
As a developer, contributing to open-source is something everyone obviously wants to do but creating and publishing something of their own to the npm registry
is the penultimate challenge any developer wants to overcome.
As such, we shall attempt to do the very same thing over in the following sections and that too in a manner that is not just a beginner approach but a completely professional and an industry-accepted way by which you can publish and document your creations on npm and do an npm i your_package_name
anytime and anywhere you want and use it through both the ESM way or the CJS way.
For the sake of an example, we shall attempt to publish a components library to the registry.
We shall be making of the following tools to get our job done:
- React: needs no introduction
- React Styleguidist: An isolated React component development environment with a living style guide. It takes care of documenting all your components without you having to worry about the UI or linking or any other kind of functionality.
- Rollup: Similar to how Webpack or any bundler bundles your code into build files, Rollup compiles small and abstract code pieces into complex bundles served from a single location.
- Typescript
- Material UI and Emotion
We begin with some configuration files:
a) tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"declaration": true,
"esModuleInterop": true,
"noEmit": false,
"skipLibCheck": true,
"strict": true,
"resolveJsonModule": true,
"target": "ESNext",
"inlineSourceMap": true,
"rootDirs": ["src"],
"baseUrl": ".",
"jsxImportSource": "@emotion/react",
"outDir": "dist",
"typeRoots": ["node_modules/@types", "@types"]
}
}
b) tsconfig.esm.json
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"declaration": false,
"module": "ESNext",
"outDir": "dist/esm"
}
}
c) tsconfig.cjs.json
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "dist"
}
}
d) tsconfig.build.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"exclude": ["./build/**", "./dist/**", "./jest/**", "**/__tests__/**"]
}
e) styleguide.config.cjs
"use strict";
const path = require("path");
const fs = require("fs");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
module.exports = {
title: "Title of your components library",
ignore: ["**/__tests__/**", "**/node_modules/**"],
exampleMode: "expand",
defaultExample: true,
skipComponentsWithoutExample: true,
styleguideComponents: {
Wrapper: path.join(__dirname, "./src/theme-provider"),
},
pagePerSection: true,
sections: fs
.readdirSync("src")
.filter(
(path) =>
fs.lstatSync(`src/${path}`).isDirectory() &&
!path.startsWith(".") &&
path !== "__tests__" &&
fs.existsSync(`src/${path}/README.md`)
)
.map((dir) => {
const name = dir
.split("-")
.map((part) => {
if (part === "cta" || part === "nba") {
return part.toUpperCase();
}
return `${part.charAt(0).toUpperCase()}${part.slice(1)}`;
})
.join("");
return {
name: name,
content: `src/${dir}/README.md`,
components: `src/${dir}/${name}.tsx`,
};
}),
getComponentPathLine: (componentPath) => {
const componentName = path.basename(componentPath, ".tsx");
return `import { ${componentName} } from "@your_org/your_package_name";`;
},
getExampleFilename: (componentPath) => {
const specificComponentExampleFile = path
.join(path.dirname(componentPath), "./README.md")
.replace();
if (fs.existsSync(specificComponentExampleFile)) {
return specificComponentExampleFile;
}
const exampleFile = path.join(componentPath, "../../README.md");
if (fs.existsSync(exampleFile)) {
return exampleFile;
}
return null;
},
propsParser: require("react-docgen-typescript").withCustomConfig(
"./tsconfig.json"
).parse,
webpackConfig: {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.js(x?)$/,
use: [
{
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
modules: false,
targets: {
node: "current",
},
},
],
"@babel/preset-react",
],
env: {
production: {
presets: ["minify"],
},
test: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
},
], // , 'source-map-loader'],
exclude: /node_modules/,
},
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: [
{
loader: "ts-loader",
options: {
transpileOnly: true,
},
},
],
},
{
test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
exclude: /node_modules/,
use: [
{
loader: "url-loader",
options: {
fallback: "file-loader",
name: "[name].[ext]",
outputPath: "fonts/",
limit: 8192,
},
},
],
},
{
test: /\.(png|jpg|gif)$/i,
exclude: /node_modules/,
use: [
{
loader: "url-loader",
options: {
limit: 8192,
},
},
],
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
plugins: [
new TsconfigPathsPlugin({
configFile: "./tsconfig.json",
}),
],
},
},
};
Now this is an important file to understand. In simple words, the styleguide config file handles how your guide interprets files and reads/parses them. The above configurations asks you to have an src
folder in your root, your theme to be in src/theme-provider
, and your entry file to be src/index.ts
.
At the same time, it ensures that any and all files in all combinations of README.md
within each folder to become the documentation for that component and the root file to be the documentation of your complete project apart from a ton of other configurations.
f) rollup.config.mjs
import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";
import url from "@rollup/plugin-url";
import svgr from "@svgr/rollup";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import { terser } from "rollup-plugin-terser";
export default [
{
input: "src/theme-provider/fonts/index.ts",
output: [
{
file: `dist/theme-provider/fonts/index.js`,
format: "cjs",
sourcemap: true,
},
{
file: `dist/esm/theme-provider/fonts/index.js`,
format: "esm",
sourcemap: true,
},
],
external: ["tslib"],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ tsconfig: "./tsconfig.build.json", declaration: false }),
svgr(),
url({
include: ["**/*.woff2"],
// setting infinite limit will ensure that the files
// are always bundled with the code, not copied to /dist
limit: Infinity,
}),
terser(),
],
},
];
As the name goes, this obviously becomes the config for rollup
that we shall be using.
g) package.json
{
"name": "@your_org/your_package_name",
"version": "1.0.0",
"main": "dist/index.js",
"src": "src/index.ts",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"scripts": {
"compile": "tsc",
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && rollup -c --bundleConfigAsCjs",
"dev": "styleguidist server --config styleguide.config.cjs",
},
"license": "UNLICENSED",
"files": [
"dist/",
"package.json"
],
"typesVersions": {
"*": {
"theme-provider": [
"./dist/theme-provider/index.d.ts"
],
"button": [
"./dist/button/index.d.ts"
],
}
},
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/esm/index.js",
"types": "./dist/index.d.ts"
},
"./theme-provider": {
"require": "./dist/theme-provider/index.js",
"import": "./dist/esm/theme-provider/index.js",
"types": "./dist/theme-provider/index.d.ts"
},
"./button": {
"require": "./dist/button/index.js",
"import": "./dist/esm/button/index.js",
"types": "./dist/button/index.d.ts"
},
},
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.0",
"@mui/lab": "^5.0.0-alpha.136",
"@mui/material": "^5.14.0",
"@mui/styles": "^5.14.0",
"@mui/system": "^5.14.0",
"classnames": "^2.3.2",
"react-is": "^18.2.0",
"react-select": "^5.7.4"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-typescript": "^11.1.3",
"@rollup/plugin-url": "^8.0.1",
"@svgr/rollup": "^8.1.0",
"@types/classnames": "^2.3.1",
"@types/node": "^20.5.1",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/react-is": "^18.2.1",
"babel-loader": "^9.1.3",
"babel-plugin-react-remove-properties": "^0.3.0",
"babel-preset-minify": "^0.5.2",
"file-loader": "^6.2.0",
"react": "^18.2.0",
"react-docgen-typescript": "^2.2.2",
"react-dom": "^18.2.0",
"react-styleguidist": "^13.1.1",
"rollup": "^3.28.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"ts-loader": "^9.4.4",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"typescript": "^5.1.6",
"url-loader": "^4.1.1",
"webpack": "^5.88.1",
"yalc": "^1.0.0-pre.53"
},
"peerDependencies": {
"@emotion/react": "^11.10.8",
"@emotion/styled": "^11.10.8",
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.129",
"@mui/material": "^5.12.3",
"@mui/styles": "^5.12.3",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"sideEffects": false
}
Obviously there's again a ton of things going on here but some of the most important takeaways from this file would be the following entries:
files: These are the files that would actually be shipped away and made available to whoever installs your package.
typesVersions and exports: Post Node 16, TS corrected prioritising
typesVersions
overexports
. As such, the said config ensures that your library works well for any project irrespective of the version of Node being utilised.
With all setups now done, all that needs to be done is create an src
folder at the root level and subsequent folders for components within such as:
Don't forget that src/index.ts
is our entry file and so whatever we export from individual index.ts files of components has to be imported an subsequently exported from src/index.ts
. A little something like:
import Button, {ButtonProps} from './button'
export {Button}
export type {ButtonProps}
We have also configured theme-provider
to be used. Would have loved to share snippets for the same here but because it would not be possible, here is a sample for how it would go about.
Now that we are all done, it is FINALLY time to publish our package which is simply a 2-step process:
- Build the project using
yarn build
ornpm run build
. - Login to your npm account using
npm login
. This is a one time process and need not be done every time. Make sure you login with the same user specified in the name of yourpackage.json
.@your_org
in this case. npm publish --access public
And voila 🎉🎉🎉🎉
Congratulations! Your package is now available on npm. Make sure to change the version number on every subsequent push to the registry.
Congrats and thanks if you made it this far and please feel free to drop any queries or comments that you come across.
Top comments (4)
I feel you are some config hero
@pengeszikra turns out I had to become one just to get all of this up and running 😅
How can we make a github repository and connect the npm package to that.
@miladxsar23 I'm not quite sure if I followed your question properly. Could you explain it in a little detail about what it is you want to achieve?