DEV Community

Anubhav
Anubhav

Posted on

Publishing your first enterprise-level NPM package

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"]
    }
}
Enter fullscreen mode Exit fullscreen mode

b) tsconfig.esm.json

{
    "extends": "./tsconfig.build.json",
    "compilerOptions": {
      "declaration": false,
      "module": "ESNext",
      "outDir": "dist/esm"
    }
}
Enter fullscreen mode Exit fullscreen mode

c) tsconfig.cjs.json

{
    "extends": "./tsconfig.build.json",
    "compilerOptions": {
      "module": "CommonJS",
      "outDir": "dist"
    }
}
Enter fullscreen mode Exit fullscreen mode

d) tsconfig.build.json

{
    "extends": "./tsconfig.json",
    "compilerOptions": {
      "rootDir": "src"
    },
    "exclude": ["./build/**", "./dist/**", "./jest/**", "**/__tests__/**"]
}
Enter fullscreen mode Exit fullscreen mode

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",
        }),
      ],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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(),
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

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}
Enter fullscreen mode Exit fullscreen mode

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:

  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.

Top comments (4)

Collapse
 
pengeszikra profile image
Peter Vivo

I feel you are some config hero

Collapse
 
anukr98 profile image
Anubhav

@pengeszikra turns out I had to become one just to get all of this up and running 😅

Collapse
 
miladxsar23 profile image
milad shiriyan

How can we make a github repository and connect the npm package to that.

Collapse
 
anukr98 profile image
Anubhav

@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?