Saturday, March 15, 2025

How to setup a monorepo app (Angular)

 Tech stacks:

  1. Angular 15+ (main framework)
  2. Nx (monorepo)
  3. ngRx (state management)
  4. cypress (e2e testing)
  5. jest (unit testing)
  6. prettier (code formatter)
  7. eslint (static code analysis)
  8. husky (git commit hook)
  9. SonarQube (code quality analysis)
  10. gitlab (CICD)
  11. strapi (CMS)
  12. lit element (UI web components)
  13. Kong (Gateway)
Workflow:
  1. Create new components with nx cli, or modify existing code (add tests if needed)
  2. Run unit test with jest, and e2e test with cypress
  3. Run sonar qube to check code quality
  4. Format the changes with prettier
  5. Commit code into appropriate feature branch (this will also trigger husky to check code format and eslint)
  6. See if the gitlab pipeline run success
  7. Create MR on gitlab
Configurations examples

1. Prettier

.prettierrc

{
"singleQuote": true,
"importOrderParserPlugins": ["typescript", "decorators-legacy"],
"importOrder": ["^@angular/(.*)$", "^@my-project/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}

 2. Eslint

.eslintrc.json

{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nrwl/nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nrwl/nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nrwl/nx/javascript"],
"rules": {}
}
]
}

3. Jest

jest.config.ts


const { getJestProjects } = require('@nrwl/jest');

export default {
projects: [
...getJestProjects(),
'<rootDir>/apps/my-app',
'<rootDir>/libs/my-ui',
'<rootDir>/libs/helpers',
],
};

 4. SonarQube

sonar-project.properties

# Required metadata
sonar.projectKey=my.project
sonar.projectName=MY-PROJECT
sonar.projectVersion=$(date +%Y-%m-%d)
sonar.host.url=http://sonar.my.domain.com:8080

# Comma-separated paths to directories with sources (required)
sonar.sources=apps, libs
sonar.exclusions=**/*.spec.*, **/*.mock.*
# Language
sonar.language=ts

# Encoding of sources files
sonar.sourceEncoding=UTF-8
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.tsconfigPath=tsconfig.base.json

sonar.coverage.exclusions=**/*.spec.*
sonar.tests.inclusions=**/*.spec.*
sonar.ts.tslint.outputPath=tslint-output.json

sonar.qualitygate.wait=true

 5. TypeScript config

tsconfig.base.json

{
"compileOnSave": false,
"compilerOptions": {
"rootDir": ".",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "es2015",
"module": "esnext",
"lib": ["es2017", "dom", "ES2018.Promise"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"noUncheckedIndexedAccess": true,
"paths": {
"@my-project/config": ["config"],
"@my-project/e2e-utils/*": [
"libs/e2e-utils/src/lib/*"
],
"@my-project/helpers": ["libs/helpers/src/index.ts"],
"@my-project/shared/ui": [
"libs/shared/ui/src/index.ts"
],
"@my-project/my-ui/*": ["libs/my-ui/src/lib/*"],
}
},
"exclude": ["node_modules", "tmp"]
}

6. Gitlab CICD

################################################### Workflow rules ###################################################
include:
- template: Workflow-rules.gitlab-ci.yml

image: $DOCKER_REGISTERY/my-image-store/nodejs:20.14.0

stages:
- install
- quality
- test
- build
- deploy

##########################[ install ]###################################
yarn:install:
stage: install
tags:
- cache_dev
variables:
GIT_DEPTH: 0
CYPRESS_INSTALL_BINARY: '0'
HUSKY_SKIP_INSTALL: '1'
script:
- |-
if [ ! -d "node_modules" ]; then
yarn config set registry http://repo.my.domain.com/repository/npm-group/ -g
yarn install --frozen-lockfile
else
echo "node_modules has been retrieved from cache. No need to launch the yarn install."
fi
cache:
- key:
files:
- yarn.lock
paths:
- node_modules/
artifacts:
paths:
- node_modules/
expire_in: 15 days

##########################[ quality ]###################################
yarn:lint:
stage: quality
tags:
- deploy_prod
needs:
- job: yarn:install
artifacts: true
before_script:
- '[ $CI_COMMIT_BRANCH = $CI_DEFAULT_BRANCH ] \
&& export BASE_COMPARE_BRANCH=remotes/origin/master~1 \
|| export BASE_COMPARE_BRANCH=remotes/origin/master'
- git fetch origin
script:
- yarn run nx -- affected --target=lint --base=$BASE_COMPARE_BRANCH --parallel
- yarn run nx -- format:check --parallel --verbose --base=$BASE_COMPARE_BRANCH
allow_failure: false
when: always

sonar:
image: $DOCKER_REGISTERY/sonarsource/sonar-scanner-cli:latest
stage: quality
tags:
- deploy_prod
- sonar
script:
- sonar-scanner -Dproject.settings=./sonar-project.properties
artifacts:
when: on_success
expire_in: 15 day
paths:
- .scannerwork/
rules:
- if: $CI_COMMIT_BRANCH == 'master'
when: always
allow_failure: true

##########################[ test ]###################################

yarn:unit-test:
extends: yarn:lint
stage: test
script: yarn run nx -- affected --target=test --base=$BASE_COMPARE_BRANCH --parallel
when: always
allow_failure: false

yarn:e2e-test-chrome:
extends: yarn:lint
stage: test
script:
- yarn run nx -- affected --target=e2e --base=origin/master --parallel
when: manual

##########################[ build ]###################################

b:dev:
stage: build
dependencies:
- yarn:install
tags:
- cache_dev
needs:
- job: yarn:install
artifacts: true
before_script:
- echo `date`
script:
- 'yarn run nx -- run-many --target=build --projects=my-project,my-other-project --configuration=dev --parallel'
- 'yarn run nx -- run-many --target=server --projects=my-project,my-other-project --configuration=dev --parallel'
after_script:
- echo `date`
- 'mkdir -p target'
- 'tar -zcf target/package.tar.gz dist'
- echo `date`
cache:
- key: nx-cache-$CI_COMMIT_BRANCH
paths:
- tmp/nx-cache
artifacts:
when: on_success
expire_in: 15 day
paths:
- target/
rules:
- when: manual

b:staging:
extends: b:dev
script:
- 'yarn run nx -- run-many --target=build --projects=my-project,my-other-project --configuration=staging --parallel'
- 'yarn run nx -- run-many --target=server --projects=my-project,my-other-project --configuration=staging --parallel'
allow_failure: false

b:preprod:
extends: b:dev
script:
- 'yarn run nx -- run-many --target=build --projects=my-project,my-other-project --configuration=preproduction --parallel'
- 'yarn run nx -- run-many --target=server --projects=my-project,my-other-project --configuration=preproduction --parallel'
allow_failure: false

b:prod:
extends: b:dev
script:
- 'yarn run nx -- run-many --target=build --projects=my-project,my-other-project --configuration=production --parallel'
- 'yarn run nx -- run-many --target=server --projects=my-project,my-other-project --configuration=production --parallel'
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: always
- when: never
- allow_failure: false

##########################[ deploy ]###################################

d:dev:
stage: deploy
variables:
GIT_STRATEGY: none
tags:
- deploy_dev
environment:
name: dev
url: https://my.domain.com:4004
needs:
- job: b:dev
artifacts: true
script:
- bash /tools/deploy-tool
rules:
- when: manual
allow_failure: true

d:staging:
extends: d:dev
environment:
name: staging
url: https://my.domain.com:4004
needs:
- job: b:staging
artifacts: true

d:preprod:
extends: d:dev
tags:
- deploy_prod
environment:
name: preprod
needs:
- job: b:preprod
artifacts: true

d:prod:
extends: d:dev
tags:
- deploy_prod
environment:
name: prod
needs:
- job: b:prod
artifacts: true
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- when: never
- allow_failure: false

 7. Husky & other run scripts

{
"name": "my-project",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"start:my-project": "ng serve --project my-project",
"e2e:my-project": "nx e2e my-project-e2e",
"affected:build": "nx affected:build",
"affected:e2e": "nx affected:e2e",
"affected:test": "nx affected:test",
"affected:lint": "nx affected:lint --parallel --uncommitted",
"affected:dep-graph": "nx affected:dep-graph",
"affected": "nx affected",
"format": "nx format:write",
"format:check": "nx format:check --parallel --uncommitted",
"update": "nx migrate latest",
"update:check": "ng update",
"dep-graph": "nx dep-graph",

},
"husky": {
"hooks": {
"pre-commit": "yarn run affected:lint && yarn run format:check"
}
},
"private": true,
"dependencies": {
"@angular-devkit/core": "15.2.11",
"@angular-devkit/schematics": "15.2.11",
"@angular/animations": "15.2.10",
"@angular/cdk": "14.2.7",
"@angular/common": "15.2.10",
"@angular/compiler": "15.2.10",
"@angular/core": "15.2.10",
"@angular/forms": "15.2.10",
"@angular/material": "14.2.7",
"winston": "3.3.3",
"zone.js": "0.11.8"
},
"devDependencies": {
"@angular-devkit/build-angular": "15.2.11",
"@angular-eslint/eslint-plugin": "14.0.4",
"@angular-eslint/eslint-plugin-template": "14.0.4",
"@angular-eslint/template-parser": "14.0.4",
"@angular/cli": "~15.2.11",
"typescript": "4.8.4",
"webpack": "^5.58.1",
"yarn": "1.22.19"
}
}

8. Kong

proxy.conf.json

{
"/api": {
"target": "https://apim-kong-staging.dev.mydomain.com:8080/api-path",
"secure": false,
"logLevel": "debug",
"pathRewrite": {
"^/api": ""
},
"headers": {
"apiKey": "apikey..."
}
}
}

 

angular.json > projects > my-project > architect > serve

"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "my-project:build",
"host": "my.local.domain.com",
"proxyConfig": "proxy.conf.json"
},