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.
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.
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
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.