Borak - software developer illustrational
Blog
24.5. 2019 / 00:00

javascript

typescript

front end

architecture

Angular

Angular 8

Architecture of SPA monorepo with Angular 7/Angular 8 and Lerna - part 1

Working in the realm of React, Redux and yarn-based monorepos made me feel things could be better, but still better than Angular, I worked with sometimes on my side-projects. Well, the time passed and I was exposed to the task of setting high-traffic solution monorepo for Angular 7/8 with possibility of scaling up easily and, of course optimized as most as possible. Among other that revealed me the things are not really the way I had thought they were.
So, the following lines are a few thoughts on the problematic of the proper setup of the Angular application leveraging the present Angular 8 tools, mostly Angular CLI.

The basic structure

To begin with, there is nothing wrong in  simply writing ng new. Angular provides the basic skeleton of fully functional SPA (single page application) following profound standards regarding functionality, scalability and distributivity. I will simply evolve the architecture of the SPA based on this basic skeleton into more sophisticated project, making some thoughts on the best possible setup in different company environments.
To be truth, each company environment and development process is different and not everywhere is possible to achieve the same structure.

The most important factors that influence the available possibilities of the generally accepted best practices are the development process itself (Agile, Watterfall etc.) the historical burden (mostly on the backend side) of previous solution and last but not least the type of the specialists on-place.
Thus, I will try to make the thoughts as generic as possible. The first in the row of the tasks awaiting successful final architecture design is understanding the tools for the development provided by the Authors of the Angular themselves. Let’s start with outline of what we have at our disposal. Namely by by the Angular CLI.

Angular CLI


As noted in the intro of this tool:

„The Angular CLI makes it easy to create an application that already works, right out of the box. It already follows our best practices!“

To install Angular CLI, simply run


npm instal -g @angular/cli

I suppose, that as almost any frontend developer, you have already the NodeJS and thus npm  installer installed. If not, here is the place to look at
https://nodejs.org/en/download/

Once the cli is installed, it’s time to look at what is under the hood.


Ng commands

In the most cases, to create a skeleton of the app, you will be ok with just running


ng new
Ng generate application 
Ng generate library 
Ng new will create a basic structure of files to start work with. However, suppose, you need to incorporate the further business requirements into the architecture, you will probably fall short and only with poor, if not hardly functional, in terms of further development, app, if you do not understand at least the basic ideas that lead to this structure and the meaning of the commands provided to scale and setup the app.

So, let’s start with the basic commands (those, who are not that new to the Angular, can skip the following section).


Ng new

This is the entry gate to the realm of the Angular app development.  After running this command you will be provided the fundamentals of your app, which you can further enhance.
If you have angular cli version > 8.0.0 installed, you will also need NodeJS version at least 8.11.3.
After dispatching the

ng new
Command, you will be prompted a few question about your new project.
The interactive wizard navigates you through the initialization, asking you the following questions:
  1. What name would you like for the project? - Self-explanatory – will be name of the root folder as well a name of the application, that will be passed to the root package.json
  2. Would you like to use Angular routing?   - This is question about, wheter you want to initialize angular router as the part of the basic setup. The Angular Router enables navigation from one view to the next as users perform application tasks. We answer YES. The special router module will created in the app aplication’s app folder. More about that later.
  3. What stylesheet format would you like to use? - There are more possibilities on how the stylesheets should be handled throughout the app. I have choosen scss. This basically means, that the webpack build setup will be set with the according preloaders.
After that, the engine burst running, This is what you will see:
picture 01 - after ng new command
 
 
 The resulting folder structure is depicted in the image 02.

picture 02 - the starting file structure of the scaffolded app
 
There are many files generated by now. We will use this setup as the starting point, which we will work with as the architecture of the app changes.

App folder

This is the main app component, that is to be rendered. As we have chosen the router module to be used in the app, the app also contains app-routing.module.ts file, that is to be used to export the routes to the app.


Assets folder

This is the folder where all assets that should be present for static loading are placed. All the items in the static folder will be present in the future distils as the result of the build of the app.

Environments folder

Environments folder contains files with the setup of the app. This files are picked during the build process based on the chosen build process environment variable (setup as the attribute of the ng build/serve command). As you can see, we have two files at our disposal to begin with.
Environments folder is picked during the build process based on the environment variable and the content of the export from this folder is injected into the app and exposed. You can influence the environment file to be picked by the build in the angular.json. For example the setup for the production build/serve (invoked with the --production flag) can be found at the following place: Architect.build.configurations.production.

Mono Repos

There are many options, when it comes to structuring your app into disparate and still cooperative and coherent parts. One of them is certainly creating a monorepo and usage of tools like Lerna of yarn workspaces.

Yarn, Lerna and tsconfig.json paths.

When it comes to splitting your monorepo to disctinct parts, the first idea is for sure to use the semantic versioning together with connecting the dependent parts through simlinks. Because of the nature of many company structures, it is more than probable, there will be more teams participating on one monorepo. In this case, the splitting the different parts into separate npm packages makes sense.
The easiest tool is simple usage of compiler options – the paths array part in your tscofing.json in the root and setup the consequent higher-level packages with extends option in the tsconfig.json.


tsconfig.json and tsconfig.app.json

Let’s make the first attempt to modularize the monorepo to individual packages, We will split the repo into individual parts and leverage the typescript’s path overrides to fling all the parts together.
We need to write top-level setup for the monorepo to run. Let’s create a top-level tsconfig.json, setup the path overwrites and link it to our local tscofnig.jsons, which are present in the individual apps.
We will go to the demo root folder and run

npm init

You will be provided a setup wizard in the console. For now we can simply run through the setup running enters everywhere.
Now, we will create the top-level tsconfig.json. Still in the demo root folder create the file tsconfig.json.
The important part in the tsconfig.json is the CompilersOptions part, where we declare the paths overwrites. Here is the content of the created and amended tsconfig.json


{
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
   },
"include": [
"packages/**/*.ts"
],
"exclude": [
"**/test.ts",
"**/*.spec.ts",
"**/*.e2e-spec.ts"
],
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
 }

Resolving modules in typescript

As stated in the typescript documentation (https://www.typescriptlang.org/docs/handbook/module-resolution.html
), there are basically two types of the module resolution:

Classic

A non-relative import to moduleB such as import { b } from "moduleB", in a source file /root/src/folder/A.ts, would result in attempting the following locations for locating "moduleB":

/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts

Nodejs

Similarly, a non-relative import will follow the Node.js resolution logic, first looking up a file, then looking up an applicable folder. So import { b } from "moduleB" in source file /root/src/moduleA.ts would result in the following lookups:

/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (if it specifies a "types" property)
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (if it specifies a "types" property)
/root/node_modules/@types/moduleB.d.ts
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts

Paths

Paths are powerfull feature of the typescript compiler, by which we can tell the engine (webpack loader in our case) how to resolve non-relative imports in our codebase. It must be mentioned that Angular uses as the main source of truth its tsconfig.app.json, that it founds in the root and which It’s cli uses to compile the final tsconfig as well as to setup some parts of the webpack configuration. To be more accurate, it searches for this file based on the proces’ cwd attribute.
However we can still use the feature of extending tsconfig.jsons by setting the “extends” property in the individual configs to the Angular setup.

So, our structure of the tsconfigs is like this:

Core:
- root/tscofnig.json – here we setup the baseUrl to ‘./’ and at the same time we setup the paths definitions in the following way:

"compilerOptions": {
   "baseUrl": "./",
   "paths": {
     "@first-app": [
       "./app1/src"
     ],
     "@second-app": [
       "./app2/src"
     ],
     "shared/*": [
       "./shared/*"
    ]
  },

Notice the format of the individual paths, that can be tricky. It is the array of strings defining the exact route. We are not limited to define the path in the exact way. Typescript compiler understands to usage of so called wild cards. You can see that in the “shared” route. The wild cards in the key means “Match everything on the path” while the wild cards in the definition string means “use here the matched value”.

Root/app1/tscofig.app.json – Here we set it up to extend the outer config


"extends": "../tsconfig.json",


Growing bigger

Let’s copy the app1 folder and past it to our demo root folder as, for example, app2.
After adding other apps to our file structure, the overall architecture could like this

picture 03 - the monorepo structure so far
 
 

Other possibilities

OK, so far good job. But as the project grows, it becomes more and more difficult to keep idea of how the relevant paths should look like and how the separate tsconfigs are interrelated.
It’s time to look at other alternatives. The most popular are probably the
  1. Lerna
  2. and Yarn

Yarn 

Yarn is package manager working with npm repository written momentarily in typescript and considered a direct competitor to npm package manager. Competition is always good and the final benefitor is always the consumer. The developer in this case.
The recent state is that many npm quirks, that yarn had solved are already handled in the recent versions of the npm package manager itself.
This fact and also a fact that to use yarn only for monorepo handling is not a really effective way to set up our environment leads us to a tool specialized just for our needs – Lerna.

Lerna

As stated in the intro documentation:
„Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.“
Looks like just what we need here. To start maintaining our monorepo with lerna is as easy as entering our root folder and typing

npx lerna init
This will initialize a lerna project by adding following items to the file structure

  1. lerna.json
  2. packages
  3. ammend package.json by adding lerna as a dev dependecy
Using Lerna, we will use the following 4 command in the begining:
  1. bootstrap – to link local packages together and install missing dependencies
  2. publish – to bump the version in the packages (if changed since the last release) and publish them.
  3. link convert – to move dev dependencies to the top directory. This way we will keep the dev dependencies up to date and allow sharing them.
For more information about lerna, please check the documentation:
https://github.com/lerna/lerna

So, let’s start by moving the app1, app2 and shared folders to the newly created packages folder. Now we need to ammend the lerna.json file as follows:


{
"packages": [
"packages/*",
"packages/shared/*"
],
"version": "0.0.0"
 }

By now, this should be the structure of our monorepo:
picture 04 - the final file strucrure - part 1
 
We added shared modules to the lerna json, so that lerna knows about them too.


More on the tsconfig.json

There are a few things good to know when setting up a little bit more sophisticated structure.

Extends
One tsconfig.json can extend another by specifiing

{extends: „../path/to/probably/less/nested/tsconfig.json“}
However, by adding the tsconfing.json file into a directory, you automatically tur it into a so called typescript project root, which has some specific traits. The most important is, that since now, all files in the directory and recursively in all other it’s subdirectories will be added to the compilation. Yep, the parent’s exlude or include cannot do nothing about it.
The only option is to define exludes allways separately in the most nested tsconfig.json for the following subdirectories.

Includes

It has some other consequencies. For example by specifiing include in the nested tsconfig.json, you automattically rewrite includes from parrents, that are ignored anyway.
All relative paths in the tsconfig.json are resolved relatively to the location of the file. Except those specified in the


{…
    “paths”: [“resolved”,”relatively”,”to”,”baseurl”]
…}

Final tsconfigs

With all this knowledge, we can now specify our own tsconfigs. We will have one in the root, and then one in the each root of the individual packages.

The root tsconfig.json will setup all common attributes:


{
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
   },
"include": [
"packages/**/*.ts"
],
"exclude": [
"**/test.ts",
"**/*.spec.ts",
"**/*.e2e-spec.ts"
],
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
 }

The individual packages will only add  nescessary details.
The packages will receive one config each  - tsconfig.app.json, that will extend the root tsconfig.json.
Here is their shape:

{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
   },
"include": [
"src/**/*.ts",
"../shared/test/*.ts"
],
"exclude": [
"src/test.ts",
"src/**/*.spec.ts"
]
 }


File angular.json

This is the last piece of the puzzles of the setup of the configuraiton files we need to work with.
There will be only one angular.json config in the root of the project. This is it’s final shape:


{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "packages",
"projects": {
"Core": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
       },
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/Core",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
           },
"configurations": {
"production": {
"fileReplacements": [
                 {
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
               ],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
                 {
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
               ]
             }
           }
         },
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "Core:build"
},
"configurations": {
"production": {
"browserTarget": "Core:build:production"
}
           }
         },
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "Core:build"
}
         },
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
           }
         },
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
           }
         },
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "Core:serve"
},
"configurations": {
"production": {
"devServerTarget": "Core:serve:production"
}
           }
         }
       }
     },
"app1": {
"projectType": "application",
"schematics": {},
"root": "packages/app1",
"sourceRoot": "packages/app1/src",
"prefix": "app1",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/app1",
"index": "packages/app1/src/index.html",
"main": "packages/app1/src/main.ts",
"polyfills": "packages/app1/src/polyfills.ts",
"tsConfig": "packages/app1/tsconfig.app.json",
"aot": false,
"assets": [
"packages/app1/src/favicon.ico",
"packages/app1/src/assets"
],
"styles": [
"packages/app1/src/styles.sass"
],
"scripts": []
           },
"configurations": {
"production": {
"fileReplacements": [
                 {
"replace": "projects/app1/src/environments/environment.ts",
"with": "projects/app1/src/environments/environment.prod.ts"
}
               ],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
                 {
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
               ]
             }
           }
         },
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "app1:build"
},
"configurations": {
"production": {
"browserTarget": "app1:build:production"
}
           }
         },
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "app1:build"
}
         },
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/app1/src/test.ts",
"polyfills": "projects/app1/src/polyfills.ts",
"tsConfig": "projects/app1/tsconfig.spec.json",
"karmaConfig": "projects/app1/karma.conf.js",
"assets": [
"projects/app1/src/favicon.ico",
"projects/app1/src/assets"
],
"styles": [
"projects/app1/src/styles.css"
],
"scripts": []
           }
         },
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/app1/tsconfig.app.json",
"projects/app1/tsconfig.spec.json",
"projects/app1/e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
           }
         },
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "projects/app1/e2e/protractor.conf.js",
"devServerTarget": "app1:serve"
},
"configurations": {
"production": {
"devServerTarget": "app1:serve:production"
}
           }
         }
       }
     }},
"defaultProject": "Core"
 }

Pretty long stuff. We are OK with the generated angular.json with some changes in the project part of the json. For the @angular-devkit to know, what loaders to use and how to interpret certain arguments passed to ng commands, we need to writte special entry for every subproject added to the file structure. The entry for our app1, for example looks like this:


app1": {
"projectType": "application",
"schematics": {},
"root": "packages/app1",
"sourceRoot": "packages/app1/src",
"prefix": "app1",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/app1",
"index": "packages/app1/src/index.html",
"main": "packages/app1/src/main.ts",
"polyfills": "packages/app1/src/polyfills.ts",
"tsConfig": "packages/app1/tsconfig.app.json",
"aot": false,
"assets": [
"packages/app1/src/favicon.ico",
"packages/app1/src/assets"
],
"styles": [
"packages/app1/src/styles.sass"
],
"scripts": []
           },
"configurations": {
"production": {
"fileReplacements": [
                 {
"replace": "projects/app1/src/environments/environment.ts",
"with": "projects/app1/src/environments/environment.prod.ts"
}
               ],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
                 {
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
               ]
             }
           }
         },
 
 

All we need to do each time, is to check and ammend the paths in the config to fit the file structure.
OK, now, with the angular.json correctly setup, we can add few commands in the scripts part of the root package.json. For the start, let’s beggin with these:

"scripts": {
"lerna:bootstrap": "lerna bootstrap",
"test": "echo "Error: no test specified" && exit 1",
"serve:app1": "ng serve app1 --port 2000"
 },

Now, if you haven’t done this already, let’s install the lerna@latest globally, because we need lerna to install dependencies and link our packages together. Run:

npm install -g lerna@latest

Now, in the root folder of the monorepo, let’s bootsrap the project:

npm run lerna:bootstrap

This should install all the packages exactly the way, so that the individual packages have access to all the dependencies and to each other. To link together two members of the monorepo, the required package must be mentioned in the relevant package.json (in the dependencies/devDependencies part). Lerna will analyze the packages and create simlinks in the relevant node_modules folders, if needed.

Containers and Components

According to the Angular documentation, there should be two basic types of components in the application:
  • Components
  • Containers
Let’s elaborate on each little bit.

Components

The components have the role of presenting the data through their views. They are dumb presentational components, that receive the data through their API surface. In the case of Angular components, in the most cases, the @Input/@Output properties are meant by this.

Containers

The containers, on the other hand, are those components, that handle the flow of the data from the state management layer and pass the data to the dumb presentational Components. These container components will be also those, that will be provided to the router. The State Management layer in our case will be the NGRX Store.

Further granularity

Each app in the monorepo will consist of other additional constructs. Let’s create a space for them for now and elaborate on them later on. Add the following folder into our ./demo/app1/src/app

  • mixins – will be used to compose the most common types of the classes through the decorator pattern. Alternative to abstract classes and Abstract Factory pattern
  • services – all the services specific for the individual apps in the monorepo.
  • enums – Typescript alternative to constants in javascript. As a matter of fact , it ‘s rather a feature missing in nowadays ECMA script implementations and the standard itself.
  • store – the state management engine. We will use NGRX store for this part.

Store

NGRX store is reactive implementation of Redux store. There are a few other parts this engine works with:        
  • reducers
  • actionCreators
  • side effects – Enable handling side effects, that are invoked by certain actions. For example a http requests. NGRX has already provided a library for these situations. It’s called @ngrx/effects and its exactly that we will we use. As reducers and actions semantically belong together implementing the handling of changes to the state in the application, we will export them by individual ECMASCRIPT modules where actions will be declared together with reducers for each individual use case.
So the file structure so far should look like this:
picture 05 - monorepo structure -further granularity
 
 

Modules and DI

We have two types of entities in our application structure right now. First are the entities that are primarily responsible for the rendering of the application in the first case. Among them belong:
  • Components
  • Containers
  • Services
We will provide these entities through Angular injector, because these are the entities to be supposed to be Injectables (components and services).
The other entities serve as a helpers for the first type of entities. They could be exported as for example providers with dependency injecton tokens, but this would mean to place the responsibility for providing them to angular injector. Thus we could not rely on the First type of entities (components, services) to work indepentently with any type of angular core module (in our case the app.module.ts). In other words we change the tight coupling of components to the specific injector setup to tight coupling among the files itself. The second case is OK for us as we do not intend to provide this helpers separately (otherwise we would have them provided in the shared package of the monorepo). On the other hand, we want the components (mostly the presentation components) to work independently of the injector. Mostly only with services that are already a part of a bunch of generally used ng modules.

So, here we prefer the convention setup to the feature setup. The feature setup attitude is, on the other way, chosen on the top level of the monorepo, where the app is split to individual packages. There is also a special shared folder for common parts of the application, which will be provided through the module system.

Modularity in Angular application

So far we were looking at the application from the convention-based point of view. We have the app module splitted into smaller parts based on their type and their role in the app.module. Lets take a more high-level point of view, where we, on the other hand, take the feature-based view on the application.
From the perspective of the future scalability, we need to reconsider right now, what types of modules we need to integrate into the overall structure of the individual applications.

Core module

The first will be the core module. In our case the Core module is the app.module.ts itself, where the application routing, DI and state management will be handled.

Shared module

There will be parts, which will be used on the different places of the application multiple times. This time this is not a shared package common for all apps in the monorepo. Only the shared parts within the individual apps. This will be mostly singleton services handling side effects other than the global state (which is to be handled by the afformentioned store and related services.). The most important part will be probably the HTTP API, which will be builded on the top of the HTTPClient service from the related Angular module.

Features Module

In our concrete case, the Features module is the Shared Module itself. This last module is meant to handle the domain-specific functionalities of the app itself. For now we need to use even more granural approach as the individual features are meant to be self-contained entities. We will accomplish that by separating them into separate modules that will ensure this application’s parts to be reusable on the most granular way. The first idea to accomplish this, would be to use reexports of the individual modules and provide them this way in the whole application by the main app.module.ts. Hovewer things does not work this way (as you can read here https://stackoverflow.com/questions/40537687/re-export-nested-modules). The solution is to include the features in the separate place and import them in the common declaration part of the main app.module.ts as well as in the module itself has to handle the imports and declarations.
So far, we have the following structure of each application in our monorepo:

picture 05 - modular structure of individual packages/ applications
 
 

Conclusion

So far, we have outlined the oveall structure of the application and successfully integrated it into the monorepo. We have learned some hot topisc about the most important parts of the monorepos and modularization in the Angular applications. We also learned some features of the Lerna tool.

In the part 2 of this tutorial, which I belive is to come soon, I will shed some light to  the following topics:
  • Setup of the npm script throughout the monorepo (based on what has already been mentioned here)
  • Setup of the tests, coverage, linting and git hooks
  • Handling the global state and setup of the Redux
  • Handling the side-effect witch @ngrx/effects
  • Little about PWA and producing the chunks of the minimal possible size
  • Where and when to use lazy loading and what are the alternatives
  • Little about 12 factor apps, their principles and how effectively aply them to our monorepo app.
  • HTTP handling, security, authorization and authetification


More by Borak

To maximalize your user experience during visit to my page, I use cookies.More info
I understand

#BORAKlive

This page is subjected to the Creative Common Licence. Always cite the Author - Do not use the page's content on commercial basis. Comply with the licence 3.0 Czech Republic.
go to top