Here's how to setup a basic TypeScript NPM React package and publish it.

I just spent the last five hours working out how to create an NPM package. Build tools and bundling are definitely one of my weaknesses when it comes to JavaScript. So I wrote this post.

Initialisation and Setup

This guide was written using NodeJS v18.16.0 and NPM v9.5.1

Firstly, create a new directory for your package. For this example I'll use test-package. Initialise the git repo if you desire and also run npm init. If you want to scope a package under your NPM username or org you can run npm init --scope=@my-username. If you want to do this afterwards, you can just change the name property in your package.json to @my-username/test-package. Since we're adding a build step, the entry point should refer to the built location of the Javascript file - I'll be using dist/index.js.

You'll need to install following packages to build your package:

npm install --save-dev @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript @types/react react rollup rollup-plugin-peer-deps-external rollup-plugin-postcss tslib typescript

And move React to the peerDependency section by modifying the package.json file. While we're here, also add the types, type, and files properties.

...
"peerDependencies": {
  "react": "^18.2.0"
},
"type": "module",
"types": "dist/index.d.ts",
"files": [ "dist" ]
...
  • Type specifies that this is an ECMAScript Module package and shouldn't use CommonJS modules.
  • Types specifies where the TypeScript typing definitions are.
  • Files specifies which files should be published to NPM.

Now you can write some simple code for your component so we can test if it works later on. You need the following three files in the src directory.

// src/index.tsx
import React from "react";
import styles from "./styles.module.css";

const TestComponent: React.FC = () => (
  <div className={styles.wrapper}>
    Hello this is your special component.
  </div>
);

export default TestComponent;
/* src/styles.module.css */
.wrapper {
  background: pink;
  color: red;
  font-family: "Comic Sans MS", cursive, sans-serif;
  font-size: 3rem;
  padding: 1rem;
}

You also need to add a type definition file for the CSS module, or you'll get warnings when trying to build.

// src/typings.d.ts
declare module "*.module.css" {
  const classes: { [key: string]: string };
  export default classes;
}

Configuring your build tools

As you might have seen from the npm install command earlier, we're using the packages Rollup and TypeScript to build our package. Create a tsconfig.json file in your project root with the following:

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "jsx": "react",
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "emitDeclarationOnly": true
  }
}

I won't go over all of these options - if you're interested the details are available here.

Next create a Rollup config file:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import external from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
  
import pkg from "./package.json" assert { type: "json" };

export default {
  input: 'src/index.tsx',
  output: [
    {
      file: pkg.main,
      format: 'esm',
      sourcemap: true,
      exports: 'default',
      name: 'react-lib'
    }
  ],
  plugins: [
    external(),
    resolve(),
    commonjs(),
    typescript({ tsconfig: './tsconfig.json' }),
    postcss()
  ]
}

This sets up all our Rollup plugins:

  • plugin-node-resolve A Rollup plugin which locates modules using the Node resolution algorithm, for using third party modules in node_modules.
  • @rollup/plugin-commonjs A Rollup plugin to convert CommonJS modules to ES6, so they can be included in a Rollup bundle.
  • @rollup/plugin-typescript A Rollup plugin for seamless integration between Rollup and Typescript.
  • rollup-plugin-peer-deps-external Automatically ensures any peerDependencies are not included in the build (e.g. we don't want to bundle react with our build since it will exist in the consuming app).
  • rollup-plugin-postcss So we can import CSS in our package.
If you're looking for more plugins, this is a good resource: https://github.com/rollup/awesome

This config file also sets up the input (aka entrypoint) and any outputs. You can have multiple outputs in different formats, for example, one for a CommonJS build and another for an ESM build. I will be consuming this library in projects that support ESM so I just have the one output.

Finally, make sure you add a main property to your package.json so Rollup knows where to save the built file. I use dist/index.js.

Now you should be able to perform a build of your application. Add the following scripts to your package.json:

...
"scripts": {
  "build": "rollup -c",
  "build:watch": "rollup -cw"
},
...

Run npm run build and see what happens! You should have a dist folder containing your built files, sourcemaps and type definitions. Since we defined the main property in the package.json we can now import this package locally in a React project.

Using the package in another project

Set up another React project somewhere so we can test out the package works. I like to use Vite.

To test your package locally, you can run npm install with a directory:
npm install /path/to/test-package

Then, you should be able to use your package just like any other! Import it into your React app and add it to one of your components.

// the @my-username/ bit is only if you scoped your package to your username
import TestComponent from '@my-username/test-package';

const App = () => (
  <>
    <TestComponent />
  </>
);

export default App;

By now you should hopefully see your component's beautiful Comic Sans in your React app.

Publishing your package

You'll need an account on npmjs.com. Login to your account in the terminal with npm login.

Then to publish, run npm publish --access public inside your package directory.

$ npm publish --access public
npm notice 
npm notice 📦  @samgl/test-package@1.0.0
npm notice === Tarball Contents === 
npm notice 96B   dist/index.d.ts  
npm notice 1.1kB dist/index.js    
npm notice 1.9kB dist/index.js.map
npm notice 715B  package.json     
npm notice === Tarball Details === 
npm notice name:          @samgl/test-package                     
npm notice version:       1.0.0                                   
npm notice filename:      samgl-test-package-1.0.0.tgz            
npm notice package size:  1.6 kB                                  
npm notice unpacked size: 3.9 kB                                  
npm notice shasum:        1e47da82e4715c3bce8e2e129e33e2f1323b3290
npm notice integrity:     sha512-BKJj/mj2xQb9f[...]pUdXVwjbKQDdA==
npm notice total files:   4                                       
npm notice 
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
Authenticate your account at:
https://www.npmjs.com/auth/cli/
Press ENTER to open in the browser...
+ @samgl/test-package@1.0.0

Tada!