Shared Configs
Sharing ESLint, TypeScript, Prettier, and other tool configurations across monorepo packages
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 linesShared 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
- Keep config packages private — Shared configs are internal; set
"private": trueso they're never accidentally published. - Use
extendschains — Build from base to specific:base -> react -> next. Each layer adds only what it needs. - Version config packages — Even if private, bumping versions helps teams track when configs change.
- Allow local overrides — Config packages set the baseline; individual packages should be able to override rules for special cases.
- Include all plugin dependencies — The config package should own its plugin dependencies so consumers don't need to install them separately.
- Document deviations — If a package overrides a shared rule, add a comment explaining why.
- 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
extendspath resolution —tsconfig.jsonresolvesextendsfromnode_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
Related Skills
Changesets
Versioning and changelog management with Changesets for coordinated monorepo package releases
CI Optimization
CI/CD optimization for monorepos including affected detection, caching strategies, and parallel execution
Dependency Management
Managing internal package dependencies, versioning strategies, and dependency graph health in monorepos
Lerna
Multi-package repository management with Lerna for versioning, publishing, and task orchestration
Nx
Monorepo development with Nx including project graph, generators, executors, and computation caching
Pnpm Workspaces
Managing monorepo packages with pnpm workspaces including linking, filtering, and dependency hoisting