Submitting a bug/feedback has never been easier! Learn more
 
Matthew Christopher Albert Matthew Christopher Albert | Aug 9, 2023

Creating NPM Private Package for Internal Usage using Vite

Introduction

Creating NPM packages for internal usage can be a daunting task, leaving many individuals unsure of where to start. However, such packages can significantly benefit organizations with multiple projects that rely on common code across various endeavors.

These internal packages are particularly useful for sharing utilities, logic, or components that serve as core, fundamental, or atomic building blocks, reducing the need for frequent updates. It's essential to focus on creating packages for reusable code and steer clear of generating packages for specific product features.

While internal packages enhance code reusability, they come with the responsibility of proper maintenance and version control to ensure seamless operations.

In our own company, we recently encountered the necessity of having a modular package that could be utilized in both frontend and backend tech stacks. As a team responsible for multiple projects, we wanted to maintain consistency by utilizing the same logic. To address this, we decided to create a comprehensive package to handle core functionalities, striving for simplicity and full test coverage.

Throughout this article, we will address the following key points:

  1. How to create the npm package project?
  2. How to build it?
  3. (Optional) Implementing Testing: Ensuring Package Integrity and Stability using Vitest
  4. How to do proper package versioning?
  5. How to deploy to a private NPM repository?
  6. How to npm install a private package within a CI/CD pipeline?

By exploring these points, we will delve into the challenges we faced and the solutions we adopted. We will also demonstrate the step-by-step process of streamlining NPM private package development using the powerful Vite framework. By the end of this article, you'll have the knowledge and tools to create robust and reusable packages tailored to your organization's specific needs.

Here’s how we do it.

1. Setup the Project

Our setup is using Vite 4 and Node 16. This is our main barebone for the project’s structure.

|- src // source code
  |- index.ts
  |- ...
|- dist // generated on build
  |- index.es.js
  |- index.umd.js
  |- types
    |- index.d.ts
    |- ...
|- package.json
|- vite.config.ts
|- tsconfig.json // tsconfig for building and developing
|- tsconfig.emit.json // tsconfig for emitting types
|- package-lock.json
|- .npmignore
|- .gitignore
|- .npmrc.example
|- .npmrc // please gitignore this

When choosing a package name if you wanted to use major package repo registries such as GitHub and NPM, you need to comply with their naming scheme: @organization_name/project_name

This naming scheme using @ prefix is meant for a scoped package, meaning that it is scoped for an organization or certain bundle.

If you use the default NPM registry, you can drop the @organization_name prefix if your package is public.

The reason is to prevent the same project name from clashing with other users. But for public NPM packages, because it’s for open-source sake it is allowed to use without prefixes.

2. Choosing Build Tools

Whether you're using Vite, Webpack, or Turbopack it's no problem.
But we picked Vite for its ease of use and our familiarity with it.

import { defineConfig } from 'vite';
const { resolve } = require('path');

export default defineConfig({
  build: {
    lib: {
      // Could also be a dictionary or array of multiple entry points
      entry: [resolve(__dirname, 'src/index.ts')],
      name: '@metapals/package_name',
      // the proper extensions will be added
      fileName: (format, entryName) => {
        // do conditionals to map entryName to the target dist filename (optional)
        return `index.${format}.js`;
      },
    },
    rollupOptions: {
      /* 
        add you required external package that 
        need to be installed on the project
        you're going to install seperatedly
        (reducing bundle size) (move this from dependencies to peerDependencies)
      */
      external: [
        'react',
        'react-dom',
      ],
    },
  },
  test: {
    setupFiles: ['./setupTests.js'],
  },
})

3. Adding Tests (optional)

Because one of our main purposes is to have fully tested code, we decided to add unit tests to our project. In this case, we used Vitest, it’s a package by Vite and compatible with Jest. We also used jest-extended for some extra assertion features.

We are faced with a challenge to usejest-extended for vitest. So we’re using this setupTests.js

import { expect } from 'vitest';
import * as matchers from 'jest-extended';
expect.extend(matchers);

The downside is we haven’t really found a way to integrate the typedef of jest-extended to the autocomplete.

So we’re using some hacky way to do it:

(expect(task1) as any).toHaveBeenCalledBefore(task2);

Yes, using any typecasting.

4. Preparing for Release

For releasing we don’t want to include our source maps, or unnecessary files to be uploaded to the repo. Therefore we utilize .npmignore to ignore what is to be uploaded.

# Usual Stuff inside .gitignore is a must e.g. node_modules, configs, env, test results
...

# Your sensitive .npmrc configuration
.npmrc
.npmrc.example

Also, you need to prepare your package.json so it doesn’t include dependecies and devDependecies the rest is fine if you wanted to keep it though.

Example of desired package.json

{
  "name": "@metapals/package_name",
  "version": "1.0.0",
  "license": "MIT",
  "main": "./dist/index.es.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.es.js",
      "require": "./dist/index.umd.js",
      "types": "./dist/types/index.d.ts"
    },
    "./new.css": "./assets/new.css",
    "./package.json": "./package.json"
  }
}

Let’s breakdown:

  • name : the package name
  • version : the package version
  • license : optional licensing if you’re package open-source
  • main : main entry point of the library
  • types : main entry point of the library type definition
  • exports : additional stuff to be enabled for use outside the entry points

Please take note that import is for ESM and require is for CommonJS.

Example usage of additional exports:

import '@metapals/package_name/new.css';

But the next question is, how do we remove the dependencies? Well, we create a script for release, that gonna be discussed in the next step.

5. Releasing

So, we created publish.sh , which in this example we’re uploading to GitHub Private NPM Repo Packages.

#!/bin/bash

# Move the package.json to package-local.json temporarily
cp package.json package-local.json

# Remove devDependencies and dependencies from package.json
node -e "const packageJson = require('./package.json'); delete packageJson.scripts; delete packageJson.dependencies; delete packageJson.devDependencies; require('fs').writeFileSync('./package.json', JSON.stringify(packageJson, null, 2))"

# Set the package name and version
PACKAGE_NAME=$(node -p -e "require('./package.json').name")
PACKAGE_VERSION=$(node -p -e "require('./package.json').version")

echo "Publishing $PACKAGE_NAME@$PACKAGE_VERSION to npm..."

# Publish the package to GitHub Packages (with error handling)
trap 'echo "Publish failed. Continuing script execution..."' ERR
npm publish --registry=https://npm.pkg.github.com --access restricted --owner=@metapals

# Move the package.json back to its original local data
rm package.json
mv package-local.json package.json

# Remove the trap
trap - ERR

Basically what we did is temporarily duplicate package.json and remove unnecessary keys before uploading, and restore it back once the upload is done.

6. Versioning Practices (optional)

It is recommended to use semver for automated package usability. Why? So you can use this syntax for installing to prevent using a higher breaking change version MAJOR or MINOR.

npm install @metapals/package_name@^1.0.0

If you use ^1.0.0(^ caret symbol) meaning that it will match any version greater than or equal to 1.0.0 and less than 2.0.0 — constraint to the latest minor update.

npm install @metapals/package_name@~1.0.0

If you use ~1.0.0 (~ tilde symbol) meaning that it will match any version greater than or equal to 1.0.0 and less than 1.1.0 — constraint to the latest patch update.

There is also another matcher such as exact, greater than equal, hyphen, and logical or, that we will not discuss in this article. However, if you’re curious you can search on semver constraint

7. CI/CD Setup

We wanted to use it on another project, but we need to npm install it on other repo CI/CD pipelines. So in this case we’re using Github Actions. We also wanted to keep our .npmrc git ignored because, on the dev PC, we wanted to don’t have to think about the token very much in our daily dev stuff. Therefore we created .npmrc.example so it can be committed to the repo.

.npmrc.example

//npm.pkg.github.com/:_authToken=my_token_here
@metapals:registry=https://npm.pkg.github.com

7a. Automatic Release

.github/workflows/release.yml

name: Release on Tag

on:
  push:
    tags:
      - v[0-9]+.[0-9]+.[0-9]+

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
        
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 16
          
      - name: Setup .npmrc
        run: cp .npmrc.example .npmrc && sed -i "s/my_token_here/${{ secrets.GITHUB_TOKEN }}/g" .npmrc
        
      - name: Install Dependencies
        run: npm install
        
      - name: Build
        run: npm run build
        
      - name: Release
        run: chmod +x ./scripts/publish.sh && ./scripts/publish.sh

Because we already created a release script, we can just straight use that for the pipeline.

7b. Installing Private NPM Package on Other Repository

.github/workflows/build-on-pr.yml

name: Build on Pull Request

on:
  pull_request:
    branches:
      - develop

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 16

      - name: Setup .npmrc
        run: cp .npmrc.example .npmrc && sed -i "s/my_token_here/${{ secrets.GITHUB_TOKEN }}/g" .npmrc

      - name: Install Dependencies
        run: npm install

      - name: Build
        run: npm run build

You can also replace ${{ secrets.GITHUB_TOKEN }} with any secrets if you have a package outside the organization / different package registry. Or if you’re comfortable with using the console environment you can replace it with the env name e.g. ${NPM_TOKEN}, so now it can also look like this

//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
@metapals:registry=https://npm.pkg.github.com

Btw, when creating this pipeline, we found out that on the GitHub Action pipeline, this way of writing files will not make \n into newline, so instead we use an example file and then use sed to replace its content.

- name: Setup .npmrc
  run: echo "//npm.pkg.github.com/:_authToken=\${GITHUB_TOKEN}\n@metapals:registry=https://npm.pkg.github.com" > .npmrc

Conclusions

In conclusion, we have successfully created an internal NPM package tailored to our company's specific needs, complete with seamless integration.

If you're interested in creating your own packages, we have provided a helpful template that you can leverage:

GitHub Repository: https://github.com/metapals/sample-npm-package

While the process may not be overly complicated, it does involve configuring various settings and following best practices, which can be challenging for those new to package development.

Although our focus was on creating a private package, it's worth mentioning that with a simple modification in the shell script, changing the parameter access to 'public', and adjusting your package registry settings, you can also publish your package as a public one.

In conclusion, the journey of developing an NPM package for internal usage has been rewarding, enabling us to streamline code reusability and enhance development efficiency. We hope that our experiences and the provided template will prove valuable in simplifying the process for you as well. Happy coding!

Keywords:
npm, private npm, vite, internal packages


Discuss "Creating NPM Private Package for Internal Usage using Vite" in MetaPals Discord
Share on:
Copyright © 2023 MetaPals.