Alpha

Back to posts

Share tsconfig and eslint in monorepo

19 November 2023 — 11 min read

Post Image

When working with a TypeScript monorepo project, it is common to have frontend apps including React, Vue.js, ..., alongside backends such as Express.js, Nest.js, ...
 
This scenario often necessitates configuring tsconfig files to esmodule for Single Page Applications (SPAs) and commonjs for Nest.js and different linting by configuring eslint. By sharing those configs within the monorepo, it will be easier to maintain and/or add more rules/config for all projects.
 

If you still do not know how to setup monorepo, check this post.

 

 

Problem

 
In monorepo structure, there are essential configuration files - tsconfig and eslint - that act as global configuration for all the projects within. And other tsconfig/eslint inside mono project will override the configuration when necessary.
 
Check this structure below:

├── packages 
│   └── utils 
│       ├── tsconfig.json // util_tsconfig 
│       └── package.json 
├── workspaces 
│   ├── nestjs 
│   │   ├── tsconfig.json // nest_tsconfig 
│   │   └── package.json 
│   └── react 
│       ├── .eslintrc.json // react_eslint 
│       ├── tsconfig.json // react_tsconfig 
│       └── package.json 
├── .eslintrc.json // root_eslint 
├── tsconfig.json // root_tsconfig 
└── package.json 

 
The root files are global configuration so it will contain all the things that all projects need.

{ 
  "compilerOptions": { 
    "allowJs": true, 
    "allowSyntheticDefaultImports": true, 
    "esModuleInterop": true, 
    "experimentalDecorators": true, 
    "forceConsistentCasingInFileNames": true, 
    "importHelpers": true, 
    "isolatedModules": true, 
    "module": "commonjs", 
    "moduleResolution": "node", 
    "noUncheckedIndexedAccess": true, 
    "removeComments": true, 
    "resolveJsonModule": true, 
    "skipLibCheck": true, 
    "sourceMap": false, 
    "strict": true, 
    "target": "esnext" 
  }, 
  "exclude": [ 
    "**/dist/*", 
    "**/node_modules/*" 
  ] 
} 
{ 
  "root": true, 
  "env": { 
    "browser": true, 
    "commonjs": true, 
    "es6": true 
  }, 
  "globals": { 
    "window": true, 
    "process": true 
  }, 
  "parser": "@typescript-eslint/parser", 
  "plugins": [ 
    "@typescript-eslint" 
  ], 
  "extends": [ 
    "eslint:recommended", 
    "plugin:@typescript-eslint/eslint-recommended", 
    "plugin:@typescript-eslint/recommended", 
    "plugin:prettier/recommended" 
  ], 
  "parserOptions": { 
    "sourceType": "module", 
    "ecmaVersion": 8 
  }, 
  "rules": { 
    "@typescript-eslint/semi": [ 
      "error", 
      "always" 
    ], 
    "@typescript-eslint/naming-convention": [ 
      "error", 
      { 
        "selector": "interface", 
        "format": [ 
          "PascalCase" 
        ], 
        "custom": { 
          "match": true, 
          "regex": "^I[A-Za-z]" 
        } 
      } 
    ], 
    "@typescript-eslint/explicit-function-return-type": "off", 
    "@typescript-eslint/no-unused-vars": [ 
      "warn", 
      { 
        "argsIgnorePattern": "^_" 
      } 
    ], 
    "no-unused-vars": [ 
      "warn", 
      { 
        "argsIgnorePattern": "^_" 
      } 
    ], 
    "comma-dangle": [ 
      "error", 
      "always-multiline" 
    ], 
    "linebreak-style": [ 
      "error", 
      "unix" 
    ], 
    "max-len": "off", 
    "new-cap": "off", 
    "no-console": "warn", 
    "object-curly-spacing": "off", 
    "require-jsdoc": "off", 
    "valid-jsdoc": "off", 
    "quotes": [ 
      "error", 
      "single", 
      { 
        "avoidEscape": true 
      } 
    ] 
  } 
} 

 
For all tsconfig / eslint within packages / workspaces, we need to extend from root files.

{ 
  "extends": "../../tsconfig.json", 
  "compilerOptions": { 
    "baseUrl": "src", 
    "jsx": "react-jsx", 
    "lib": [ 
      "DOM", 
      "DOM.Iterable", 
      "ESNext" 
    ] 
  }, 
  "include": [ 
    "src" 
  ] 
} 
{ 
  "root": true, 
  "extends": [ 
    "../../.eslintrc.json" 
  ] 
} 

In this approach, it is difficult to get the path of the root files to extend, especially when we have many nested folders. Because whenever we copy the content to nested folder, we need to change the relative path.
 
The project is indeep a monorepo project, wouldn't it be better to make the configuration as a singular package within?
 

Special thanks to Turborepo team for this approach.

 

 

Configuration as a package

 
We will take advantage of monorepo benefit by creating a @config package and create tsconfig and eslint inside that package.
 
First, create tsconfig package and those files:

{ 
  "compilerOptions": { 
    "allowJs": true, 
    "allowSyntheticDefaultImports": true, 
    "esModuleInterop": true, 
    "experimentalDecorators": true, 
    "forceConsistentCasingInFileNames": true, 
    "importHelpers": true, 
    "isolatedModules": true, 
    "module": "commonjs", 
    "moduleResolution": "node", 
    "noUncheckedIndexedAccess": true, 
    "removeComments": true, 
    "resolveJsonModule": true, 
    "skipLibCheck": true, 
    "sourceMap": false, 
    "strict": true, 
    "target": "esnext" 
  }, 
  "exclude": [ 
    "**/dist/*", 
    "**/node_modules/*", 
    "scripts/*" 
  ] 
} 
{ 
  "extends": "./tsconfig.base.json", 
  "compilerOptions": { 
    "declaration": true, 
    "emitDecoratorMetadata": true, 
    "incremental": true, 
    "noFallthroughCasesInSwitch": false, 
    "noImplicitAny": false, 
    "paths": { 
      "~/*": [ 
        "./*" 
      ] 
    }, 
    "sourceMap": true, 
    "strictBindCallApply": false, 
    "strictNullChecks": false 
  } 
} 
{ 
  "extends": "./tsconfig.base.json", 
  "compilerOptions": { 
    "module": "esnext", 
    "noEmit": true, 
    "verbatimModuleSyntax": true 
  } 
} 
{ 
  "extends": "./tsconfig.es.json", 
  "compilerOptions": { 
    "jsx": "react-jsx", 
    "lib": [ 
      "DOM", 
      "DOM.Iterable", 
      "ESNext" 
    ], 
    "paths": { 
      "~/*": [ 
        "./*" 
      ] 
    } 
  } 
} 
{ 
  "name": "@config/tsconfig", 
  "version": "0.0.0", 
  "license": "MIT", 
  "publishConfig": { 
    "access": "public" 
  } 
} 

 
Next, create eslint package and those files:

Note: eslint extension naming convention is eslint-config-* where * is custom name

{ 
  "root": true, 
  "env": { 
    "browser": true, 
    "commonjs": true, 
    "es6": true 
  }, 
  "globals": { 
    "window": true, 
    "process": true 
  }, 
  "parser": "@typescript-eslint/parser", 
  "plugins": [ 
    "@typescript-eslint" 
  ], 
  "extends": [ 
    "eslint:recommended", 
    "plugin:@typescript-eslint/eslint-recommended", 
    "plugin:@typescript-eslint/recommended", 
    "plugin:prettier/recommended" 
  ], 
  "parserOptions": { 
    "sourceType": "module", 
    "ecmaVersion": 8 
  }, 
  "rules": { 
    "@typescript-eslint/semi": [ 
      "error", 
      "always" 
    ], 
    "@typescript-eslint/naming-convention": [ 
      "error", 
      { 
        "selector": "interface", 
        "format": [ 
          "PascalCase" 
        ], 
        "custom": { 
          "match": true, 
          "regex": "^I[A-Za-z]" 
        } 
      } 
    ], 
    "@typescript-eslint/explicit-function-return-type": "off", 
    "@typescript-eslint/no-unused-vars": [ 
      "warn", 
      { 
        "argsIgnorePattern": "^_" 
      } 
    ], 
    "no-unused-vars": [ 
      "warn", 
      { 
        "argsIgnorePattern": "^_" 
      } 
    ], 
    "comma-dangle": [ 
      "error", 
      "always-multiline" 
    ], 
    "linebreak-style": [ 
      "error", 
      "unix" 
    ], 
    "max-len": "off", 
    "new-cap": "off", 
    "no-console": "warn", 
    "object-curly-spacing": "off", 
    "require-jsdoc": "off", 
    "valid-jsdoc": "off", 
    "quotes": [ 
      "error", 
      "single", 
      { 
        "avoidEscape": true 
      } 
    ] 
  } 
} 
{ 
  "root": true, 
  "plugins": [ 
    "react", 
    "import", 
    "jsx-a11y", 
    "react-hooks" 
  ] 
} 
{ 
  "name": "@config/eslint-config-custom", 
  "version": "0.0.0", 
  "license": "MIT", 
  "publishConfig": { 
    "access": "public" 
  }, 
  "devDependencies": { 
    "@typescript-eslint/eslint-plugin": "^6.9.0", 
    "@typescript-eslint/parser": "^6.9.0", 
    "eslint-config-prettier": "^9.0.0", 
    "eslint-config-react-app": "^7.0.1", 
    "eslint-plugin-import": "^2.29.0", 
    "eslint-plugin-jsx-a11y": "^6.7.1", 
    "eslint-plugin-prettier": "^5.0.1", 
    "eslint-plugin-react": "^7.33.2", 
    "eslint-plugin-react-hooks": "^4.6.0" 
  } 
} 

 
At this moment, we have tsconfig for Nest.js, React and util packages that uses esmodule. And eslint for all projects and eslint for React only.
 
Next, install those two packages to root:

pnpm -w add -D @config/eslint-config-custom @config/tsconfig 

 
Now, apply those configurations to all other tsconfig / eslint files.
 

1{ 
2  "compilerOptions": { 
3    "allowJs": true, 
4    "allowSyntheticDefaultImports": true, 
5    "esModuleInterop": true, 
6    "experimentalDecorators": true, 
7    "forceConsistentCasingInFileNames": true, 
8    "importHelpers": true, 
9    "isolatedModules": true, 
10    "module": "commonjs", 
11    "moduleResolution": "node", 
12    "noUncheckedIndexedAccess": true, 
13    "removeComments": true, 
14    "resolveJsonModule": true, 
15    "skipLibCheck": true, 
16    "sourceMap": false, 
17    "strict": true, 
18    "target": "esnext" 
19  }, 
20  "exclude": [ 
21    "**/dist/*", 
22    "**/node_modules/*" 
23  ] 
24  "extends": "@config/tsconfig/tsconfig.base.json" 
25} 
1{ 
2  "root": true, 
3  "env": { 
4    "browser": true, 
5    "commonjs": true, 
6    "es6": true 
7  }, 
8  "globals": { 
9    "window": true, 
10    "process": true 
11  }, 
12  "parser": "@typescript-eslint/parser", 
13  "plugins": [ 
14    "@typescript-eslint" 
15  ], 
16  "extends": [ 
17    "eslint:recommended", 
18    "plugin:@typescript-eslint/eslint-recommended", 
19    "plugin:@typescript-eslint/recommended", 
20    "plugin:prettier/recommended" 
21  ], 
22  "parserOptions": { 
23    "sourceType": "module", 
24    "ecmaVersion": 8 
25  }, 
26  "rules": { 
27    "@typescript-eslint/semi": [ 
28      "error", 
29      "always" 
30    ], 
31    "@typescript-eslint/naming-convention": [ 
32      "error", 
33      { 
34        "selector": "interface", 
35        "format": [ 
36          "PascalCase" 
37        ], 
38        "custom": { 
39          "match": true, 
40          "regex": "^I[A-Za-z]" 
41        } 
42      } 
43    ], 
44    "@typescript-eslint/explicit-function-return-type": "off", 
45    "@typescript-eslint/no-unused-vars": [ 
46      "warn", 
47      { 
48        "argsIgnorePattern": "^_" 
49      } 
50    ], 
51    "no-unused-vars": [ 
52      "warn", 
53      { 
54        "argsIgnorePattern": "^_" 
55      } 
56    ], 
57    "comma-dangle": [ 
58      "error", 
59      "always-multiline" 
60    ], 
61    "linebreak-style": [ 
62      "error", 
63      "unix" 
64    ], 
65    "max-len": "off", 
66    "new-cap": "off", 
67    "no-console": "warn", 
68    "object-curly-spacing": "off", 
69    "require-jsdoc": "off", 
70    "valid-jsdoc": "off", 
71    "quotes": [ 
72      "error", 
73      "single", 
74      { 
75        "avoidEscape": true 
76      } 
77    ] 
78  } 
79  "extends": [ 
80    "@config/custom/base" 
81  ] 
82} 
1{ 
2  "extends": "../../tsconfig.json", 
3  "extends": "@config/tsconfig/tsconfig.react-app.json", 
4  "compilerOptions": { 
5    "baseUrl": "src", 
6    "jsx": "react-jsx", 
7    "lib": [ 
8      "DOM", 
9      "DOM.Iterable", 
10      "ESNext" 
11    ] 
12    "baseUrl": "src" 
13  }, 
14  "include": [ 
15    "src" 
16  ] 
17} 
1{ 
2  "root": true, 
3  "extends": [ 
4    "../../.eslintrc.json" 
5    "@config/custom/base", 
6    "@config/custom/react" 
7  ] 
8} 
1{ 
2  "extends": "@config/tsconfig/tsconfig.nest.json", 
3  "compilerOptions": { 
4    "baseUrl": "src", 
5    "outDir": "dist" 
6  }, 
7  "include": [ 
8    "src" 
9  ] 
10} 
1{ 
2  "extends": "@config/tsconfig/tsconfig.es.json", 
3  "compilerOptions": { 
4    "declaration": true, 
5    "outDir": "lib" 
6  }, 
7  "include": [ 
8    "src" 
9  ], 
10  "exclude": [ 
11    "lib", 
12    "node_modules" 
13  ] 
14} 

 
As you can see, our configuration files are clean now, easy to read, maintain and easy to create new one when we have new package/project.
 
Here the full structure after reworking:

├── packages 
│   ├── @config 
│   │   ├── eslint-config-custom 
│   │   │   ├── base.json 
│   │   │   ├── package.json 
│   │   │   └── react.json 
│   │   └── tsconfig 
│   │       ├── package.json 
│   │       ├── tsconfig.base.json 
│   │       ├── tsconfig.es.json 
│   │       ├── tsconfig.nest.json 
│   │       └── tsconfig.react-app.json 
│   └── utils 
│       ├── tsconfig.json // util_tsconfig 
│       └── package.json 
├── workspaces 
│   ├── nestjs 
│   │   ├── tsconfig.json // nest_tsconfig 
│   │   └── package.json 
│   └── react 
│       ├── .eslintrc.json // react_eslint 
│       ├── tsconfig.json // react_tsconfig 
│       └── package.json 
├── .eslintrc.json // root_eslint 
├── tsconfig.json // root_tsconfig 
└── package.json 

 

 

Wrap up

 
In this context, we learn how to share the configuration across to the projects within monorepo. This approach will make the configuration of mono project clean and easy to maintain especially when we create new mono project.
 
Special thanks to Turborepo team again for providing the example about config turbo for monorepo so this post is created.


Or

Alpha

Fullstack developer who converts pepsi into code.


Or

Contact

General