Publishing ESLint rules to npm using pnpm monorepo

Publishing ESLint rules to npm using pnpm monorepo


Background

At work (Codebuddy), we wanted to create shareable ESLint rules to be used across the projects instead of copy pasting the rules each time. This can be achieved by following the Official shareable docs, but I wanted to outline how to achieve the same using a monorepo and also mention some issues which I faced along the way.

Creating packages

This setup has been inspired by antfu's eslint-config where he too has used a monorepo. Let us first begin by creating a pnpm workspace by creating these files

pnpm-workspace.yaml

packages:
 - "packages/*"

package.json

{
  "name": "your-eslint-configs",
  "version": "1.0.0",
  "description": "Common ESLint configs",
  "license": "MIT",
  "keywords": [
    "eslint",
    "eslint-config"
  ]
}

.npmrc

auto-install-peers=true
use-lockfile-v6=true

Then create a packages directory with all the necessary packages. For now we will be creating 2 packages: base and react:

base

index.js

module.exports = {
  env: {
    es2021: true,
    browser: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'airbnb',
    'plugin:import/recommended',
    'plugin:promise/recommended',
    // This disables the formatting rules in ESLint that Prettier is going to be responsible for handling.
    // Make sure it's always the last config, so it gets the chance to override other configs.
    'plugin:prettier/recommended',
    'prettier',
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  rules: {
    'prettier/prettier': [
      'error',
      {
        endOfLine: 'auto',
      },
    ],
    'max-len': 'off',
    'linebreak-style': ['error', 'unix'],
    quotes: ['error', 'single', { avoidEscape: true }],
    semi: ['error', 'always'],
    'object-curly-newline': 'off',
    'arrow-parens': 'off',
    'implicit-arrow-linebreak': 'off',
    'no-nested-ternary': 'off',
    'nonblock-statement-body-position': ['error', 'any'],
    camelcase: 'error',
    'consistent-return': 0,
    'no-restricted-syntax': 'off',
    'no-console': ['error', { allow: ['warn', 'error'] }],
    'arrow-body-style': ['error', 'as-needed'],
    'no-unused-vars': [
      'error',
      {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
        caughtErrorsIgnorePattern: '^_',
      },
    ],
    'no-shadow': 'error',
    'no-underscore-dangle': 'off',
  },
};

package.json

{
  "name": "@your-org/eslint-config-base",
  "version": "1.0.0",
  "description": "Common JS ESLint rules",
  "license": "MIT",
  "main": "index.js",
  "keywords": [
    "eslint",
    "eslint-config"
  ],
  "peerDependencies": {
    "eslint": ">=8",
    "eslint-config-airbnb": ">=19.0.4",
    "eslint-plugin-import": ">=2.29.1",
    "eslint-plugin-prettier": ">=5.1.2",
    "eslint-plugin-promise": ">=6.1.1"
  },
  "engines": {
    "node": ">=18"
  }
}

If you check the name field, we're scoping it to an org name.

After installing the packages by running pnpm i from the root of the directory, below is the directory structure:

node_modules
packages/
└── base/
    ├── index.js
    └── package.json
.npmrc
package.json
pnpm-lock.json
pnpm-workspace.yaml

Use the above base into react package:

react

index.js

module.exports = {
  extends: [
    'plugin:react/recommended',
    'plugin:jsx-a11y/recommended',
    '@your-org/eslint-config-base',
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['react'],
  rules: {
    'react/function-component-definition': [
      2,
      {
        namedComponents: 'arrow-function',
        unnamedComponents: 'arrow-function',
      },
    ],
    'react/prop-types': 'off',
    'react/no-unstable-nested-components': 'off',
    'react/react-in-jsx-scope': 'off',
    'react/require-default-props': 'off',
    'react/jsx-props-no-spreading': 'off',
    'import/no-extraneous-dependencies': ['error', { devDependencies: ['vite.config.js'] }],
  },
};

In the extends array, put the config at the last so that it overrides the rules. If you have included a plugin in an earlier config, do not include it in the plugins array else, it will cause issues while linting.

To use @your-org/eslint-config-base, add it as a dependency. To do so, go into packages/react and run pnpm add @your-org/eslint-config-base. This will automatically install the package from the workspace. Below is the package.json:

react/package.json

{
  "name": "@your-org/eslint-config-react",
  "version": "1.0.0",
  "description": "Common ESLint rules for React with Vite",
  "license": "MIT",
  "main": "index.js",
  "keywords": [
    "eslint",
    "eslint-config"
  ],
  "peerDependencies": {
    "eslint": ">=8",
    "eslint-plugin-jsx-a11y": ">=6.8.0",
    "eslint-plugin-react": ">=7.33.2"
  },
  "engines": {
    "node": ">=18"
  },
  "dependencies": {
    "@your-org/eslint-config-base": "workspace:^"    
  }
}

And the updated directory structure:

node_modules
packages/
├── base/
│   ├── index.js
│   └── package.json
└── react/
    ├── index.js
    └── package.json
.npmrc
package.json
pnpm-lock.json
pnpm-workspace.yaml

Using the packages in a project to test it

Now that the configs are created, next step would be to use them in a project and see whether they are working or not. To do so, create a Vite project with React template. To link the package, check the commands here or follow the below commands.

  1. Go into the created project, and run pnpm add -D <path-to-your-directory>/packages/react to install it from the packages dir.

  2. Add it to .eslintrc.js like so:

.eslintrc.js

module.exports = {
  extends: ['@your-org/eslint-config-react'],
};

Publishing the packages

To publish the packages, go into each package and run pnpm publish --access public . If everything is set up correctly, then it will be published 🥳. Below are some issues which I faced while publishing. Do check if you run into any of the issues

I tried running npm adduser and npm login, however I got these errors

> npm login
npm notice Log in on https://registry.npmjs.org/
Login at:
https://www.npmjs.com/login?next=/login/cli/c2db3009-70e8-41ea-9194-04abd03d7c5c
Press ENTER to open in the browser...
npm ERR! code ERR_INVALID_ARG_TYPE
npm ERR! The "file" argument must be of type string. Received undefined
npm ERR! Cannot read properties of undefined (reading 'stdin')

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/neeraj/.npm/_logs/2023-12-11T16_39_39_060Z-debug-0.log

Fortunately I found this Solution. Check if .npmrc exists if it exsist, then add the below line to it else create it and add the below line to it:

//registry.npmjs.org/:_authToken=token_here

If you want to see how Codebuddy has implemented the same for Next.js and TypeScript, do check it out on GitHub