Saturday, May 31, 2025

Interview Limix (frontend dev)

The Thailand based start-up named Limix is very young (less than one year old!) and somehow I got an interview invitation from a Vietnamese head hunter. She had a short call to introduce the company, its benefits and the interview process. (they provided a 3-star hotel appartment for their employees, impressive!) 

There was some uncertainties about this company and I am a bit worried but I decided to take the chance anyways.

The interviewers were 2 guys, but they did not show their webcam, only me was the one showing face (to prevent cheating, you know, now AI is everywhere and it’s really intelligent). 


They asked me about my work, what are the challenges and how did I overcome them. Also the questions focused on SEO and web optimization (I don’t have much experience on these fields anyway). 


Lastly they asked if I am willing to switch to Vue because they are using this framework, and gave me a home test (using Vue of course). The test was about cloning  bitis.com.vn site using nuxt3 and vue3, with lighthouse score to be 100. I had one week to complete the test.


I tried my best to finish (worked my ass off really!). But I did not pass the test eventually.

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"
},