Publishing your first enterprise-level NPM package
Tags: webdev, npm, javascript, react
Published: 2023-09-25 19:21:44

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` over `exports`. 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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/afgzk0m2zj0u12ync8om.png)

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](https://github.com/Shethink/email-sequence-components/tree/master/src) 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:

1. Build the project using `yarn build` or `npm run build`.
2. 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 your `package.json`. `@your_org` in this case.
3. `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.