Skip to main content
Technology & EngineeringMonorepo325 lines

Shared Configs

Sharing ESLint, TypeScript, Prettier, and other tool configurations across monorepo packages

Quick Summary30 lines
You are an expert in creating and maintaining shared configuration packages for ESLint, TypeScript, Prettier, and other tools across monorepo workspaces.

## Key Points

1. **Keep config packages private** — Shared configs are internal; set `"private": true` so they're never accidentally published.
2. **Use `extends` chains** — Build from base to specific: `base -> react -> next`. Each layer adds only what it needs.
3. **Version config packages** — Even if private, bumping versions helps teams track when configs change.
4. **Allow local overrides** — Config packages set the baseline; individual packages should be able to override rules for special cases.
5. **Include all plugin dependencies** — The config package should own its plugin dependencies so consumers don't need to install them separately.
6. **Document deviations** — If a package overrides a shared rule, add a comment explaining why.
7. **Test configs with a canary package** — Create a small test package that exercises the config to catch breakage before it affects the whole repo.
- **ESLint plugin resolution** — ESLint resolves plugins relative to the config file, not the consuming package. Use `require.resolve()` or ensure plugins are hoisted.
- **TypeScript `extends` path resolution** — `tsconfig.json` resolves `extends` from `node_modules`. Ensure the config package is a declared dependency.
- **Circular extends** — A config that extends itself or creates a loop will cause cryptic errors. Keep the chain linear.
- **Forgetting to rebuild** — If a shared config uses TypeScript, it must be built before consumers can use it. Add it as a build dependency.
- **Over-centralizing** — Not every rule belongs in the shared config. Framework-specific or package-specific rules should stay local.

## Quick Example

```javascript
module.exports = require('@myorg/prettier-config');
```

```json
{
  "prettier": "@myorg/prettier-config"
}
```
skilldb get monorepo-skills/Shared ConfigsFull skill: 325 lines
Paste into your CLAUDE.md or agent config

Shared Configs — Monorepo Management

You are an expert in creating and maintaining shared configuration packages for ESLint, TypeScript, Prettier, and other tools across monorepo workspaces.

Core Philosophy

Overview

In a monorepo, duplicating configuration across dozens of packages leads to drift and maintenance burden. Shared config packages centralize tool settings (ESLint, TypeScript, Prettier, Vitest, etc.) so every package inherits a consistent baseline. Packages can extend or override the shared config as needed.

Setup & Configuration

Directory Structure

packages/
  config-eslint/
    package.json
    base.js
    react.js
    node.js
    index.js
  config-typescript/
    package.json
    base.json
    react.json
    node.json
  config-prettier/
    package.json
    index.js

Core Patterns

Shared ESLint Configuration

packages/config-eslint/package.json:

{
  "name": "@myorg/eslint-config",
  "version": "1.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-import": "^2.29.0"
  }
}

packages/config-eslint/base.js:

/** @type {import("eslint").Linter.Config} */
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'import'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
  ],
  rules: {
    'no-console': 'warn',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    'import/order': ['error', {
      'groups': ['builtin', 'external', 'internal', 'parent', 'sibling'],
      'newlines-between': 'always',
      'alphabetize': { order: 'asc' }
    }],
    '@typescript-eslint/consistent-type-imports': 'error',
  },
  ignorePatterns: ['dist/', 'node_modules/', '*.js'],
};

packages/config-eslint/react.js:

/** @type {import("eslint").Linter.Config} */
module.exports = {
  extends: [
    './base.js',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
  ],
  settings: {
    react: { version: 'detect' },
  },
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
  },
};

Consuming package .eslintrc.js:

module.exports = {
  root: true,
  extends: [require.resolve('@myorg/eslint-config/react')],
  parserOptions: {
    project: './tsconfig.json',
  },
};

ESLint Flat Config (ESLint 9+)

packages/config-eslint/flat/base.mjs:

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettierConfig from 'eslint-config-prettier';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  prettierConfig,
  {
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/consistent-type-imports': 'error',
    },
  },
  {
    ignores: ['dist/', 'node_modules/'],
  }
);

Consuming package eslint.config.mjs:

import baseConfig from '@myorg/eslint-config/flat/base.mjs';

export default [
  ...baseConfig,
  {
    rules: {
      // Package-specific overrides
    },
  },
];

Shared TypeScript Configuration

packages/config-typescript/base.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "exclude": ["node_modules", "dist"]
}

packages/config-typescript/react.json:

{
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "noEmit": true
  }
}

packages/config-typescript/node.json:

{
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "types": ["node"]
  }
}

Consuming package tsconfig.json:

{
  "extends": "@myorg/typescript-config/react.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Shared Prettier Configuration

packages/config-prettier/index.js:

/** @type {import("prettier").Config} */
module.exports = {
  semi: true,
  singleQuote: true,
  trailingComma: 'all',
  printWidth: 100,
  tabWidth: 2,
  arrowParens: 'always',
  plugins: ['prettier-plugin-tailwindcss'],
};

Consuming package .prettierrc.js:

module.exports = require('@myorg/prettier-config');

Or in package.json:

{
  "prettier": "@myorg/prettier-config"
}

Shared Vitest Configuration

packages/config-vitest/base.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
      },
    },
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
  },
});

Consuming package vitest.config.ts:

import { defineConfig, mergeConfig } from 'vitest/config';
import baseConfig from '@myorg/vitest-config/base';

export default mergeConfig(baseConfig, defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./test/setup.ts'],
  },
}));

Best Practices

  1. Keep config packages private — Shared configs are internal; set "private": true so they're never accidentally published.
  2. Use extends chains — Build from base to specific: base -> react -> next. Each layer adds only what it needs.
  3. Version config packages — Even if private, bumping versions helps teams track when configs change.
  4. Allow local overrides — Config packages set the baseline; individual packages should be able to override rules for special cases.
  5. Include all plugin dependencies — The config package should own its plugin dependencies so consumers don't need to install them separately.
  6. Document deviations — If a package overrides a shared rule, add a comment explaining why.
  7. Test configs with a canary package — Create a small test package that exercises the config to catch breakage before it affects the whole repo.

Common Pitfalls

  • ESLint plugin resolution — ESLint resolves plugins relative to the config file, not the consuming package. Use require.resolve() or ensure plugins are hoisted.
  • TypeScript extends path resolutiontsconfig.json resolves extends from node_modules. Ensure the config package is a declared dependency.
  • Circular extends — A config that extends itself or creates a loop will cause cryptic errors. Keep the chain linear.
  • Forgetting to rebuild — If a shared config uses TypeScript, it must be built before consumers can use it. Add it as a build dependency.
  • Over-centralizing — Not every rule belongs in the shared config. Framework-specific or package-specific rules should stay local.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add monorepo-skills

Get CLI access →