Recently we moved our platform from a mono repo to a micro frontend architecture. One of the things we wanted to add was a base linter that is shared across our apps to ensure a more standardized code as both the platform and the number of micro applications expand.
In this post I'll share the basic rundown of the things we had to do in order to get the linter up and running.
1. Transition from TSLint to ESLint
As noted in the ng lint Angular Docs, TSLint is being deprecated in favor of ESLint. The migration is rather straightforward and boils down to these two lines:
installing the schematics
ng add @angular-eslint/schematics
and running the converter
ng g @angular-eslint/schematics:convert-tslint-to-eslint
For a more detailed migration guide, see this article: https://dev.to/gsarciotto/migrating-and-configuring-eslint-with-angular-11-3fg1
2. Add Super-Linter to GitHub Actions
Setting up Super-Linter was super easy, since we already had a workflows test-pull-req.yml
file that tests our build on each pull request. Adding the linter was merely adding another step to the process.
name: Test And Lint Pull Request
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
...
- name: Lint Code Base
uses: github/super-linter@v3
env:
NODE_ENV: production
VALIDATE_ALL_CODEBASE: false
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LINTER_RULES_PATH: /
TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.js
VALIDATE_TYPESCRIPT_ES: true
...
The Super-Linter docs are pretty self explanatory, the only additional thing we added here is the NODE_ENV
variable, that will be used a bit later. The VALIDATE_ALL_CODEBASE
variable came in nicely as the linter lints only files changed, so it makes adding new linter rules a bit more easier.
At this point you are good to go, you have migrated to the new ESLint and your files are being linted on each Pull Request. Tap yourself on the back!
3. Share and extend the base lint rules
Since we have around 10 applications, adding or changing a rule requires us to change it in each of the 10 applications; ain't nobody got time for that!
When we switched to the micro frontend platform we started utilizing our own Angular library for some of the configs, components, pipes and services that we use around the platform. We also keep a part of our Tailwind(❤) config in our libraries assets
folder, so that was the obvious place to put our base lint config as well.
One thing to note here is, to make your library include the assets
folder in the final dist
you have to explicitly tell it to.
This happens in the libraries ng-package.json
{
"dest": "../../dist/app-library",
"assets": ["./assets"],
"lib": {
"entryFile": "src/public-api.ts"
}
}
Now each application's eslintrc.js
can reference and extend the base-eslint.js
using the extends
property
module.exports = {
root: true,
extends: [
"./node_modules/@cognism/app-library/assets/configs/linter/base-eslint.js"
],
...
}
4. Add application specific rules
This is how our most basic eslintrc.js
config looks like in one of our micro applications.
module.exports = {
root: true,
extends: [
"./node_modules/@cognism/app-library/assets/configs/linter/base-eslint.js"
],
overrides: [
{
files: ["*.ts"],
rules: {
"@angular-eslint/component-selector": [
"error",
{
type: "element",
prefix: "app-nav",
style: "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
type: "attribute",
prefix: "appNav",
style: "camelCase"
}
]
}
}
]
}
As you can see, first we extend our base-eslint.js
rules and then we override it with our application specific rules. In this case we just want to have application specific prefixes for both components and directives.
5. Add environment dependent rules
The idea here was to enable different rule behaviors depending on the environment. For example the console.log
. We don't want the log statement committed to the main branch, but we also don't want to give out errors to developers while writing logs in their local environment.
The easiest way to do it was by simply using a ternary operator inside the lint file. Note that your config file must be in .js
format and not in the default .json
format to be able to do this.
module.exports = {
...
rules: {
"@typescript-eslint/naming-convention": [
"error",
{ "selector": "enumMember", "format": ["UPPER_CASE"] }
],
"@angular-eslint/no-empty-lifecycle-method": "off",
"no-console": process.env.NODE_ENV === 'production' ? "error" : 'warn'
}
...
}
As you can see, this is where the NODE_ENV
kicks in which we defined in our GitHub Actions test-pull-req.yml
file.
We did implement different environments this way, but are also aware that it might get messy with a lot of ternaries in different rules. If that comes to be the case, we ll just start using two files, eg. eslintrc.js
and prod-eslintrc.js
and the test-pull-req.yml
file will always point to prod-eslintrc.js
while in development we'll use eslintrc.js
.
6. There you have it!
- We used single-spa https://single-spa.js.org/ to move our monolith to the front-end microservices world.
- The Angular version used was v11.
- If you need any additional info feel free to reach out.
- Any comments and improvements are welcome.
Feel free to connect 👋
Top comments (2)
Hi Petar,
i'm a bit confused and wondering why would you migrate to single-spa if you were already using Angular? Why didn't you switch to Angular workspaces ? This is the entire reason its there. Provide you with the ability to construct your monolithic SPA into smaller chunks of Angular frontends and you could also use global routing
Hey, thanks for the question and sorry for the late reply. We wanted to have the option to plug in an application written in something other than Angular, like React of Vue, and thats where single-spa plays nicely since its main router is framework agnostic.