Building an extensible Dynamic Pluggable Enterprise Application with Angular
In this article, we will talk about how we can leverage Angular CLI build tools to create an AOT precompiled Angular plugin, which can share common code with other plugins and even work with Angular universal. This is an unofficial solution, but it works well for our case.

Building an extensible Dynamic Pluggable Enterprise Application with Angular
In this article, we will talk about how we can leverage Angular CLI build tools to create an AOT precompiled Angular plugin, which can share common code with other plugins and even work with Angular universal. This is an unofficial solution, but it works well for our case.


In this article, we will talk about how we can leverage Angular CLI build tools to create an AOT precompiled Angular plugin, which can share common code with other plugins and even work with Angular universal. This is an unofficial solution, but it works well for our case.


AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
Here’s a simple representation of what we’re building:


Here we have a simple page where we get plugins configuration from a plugins-config.json
file. Then we first lazy load the AOT compiled plugin (plugin1.js
) which has a dependency to shared.js
. The shared library contains a Tabs component and Tabs ng factories. Sometime later we load the second plugin(plugin2.js
) that reuses the code from the previously loaded shared.js
library.
Here’s the angular-plugin-architecture Github repository if you want to take a look at the source code.
We use Angular CLI 7.3.6 in the demo
Let’s to not wait for Ivy but instead deal with the current ViewEngine todayLink to this section
What are we going to discuss here?
- Why Angular?
- The goal
- What’s a plugin?
- Why is it so hard to create a plugin with Angular?
- Requirements
- Other solutions
- Towards a solution
- Single bundle
- Externals
- Dynamic exports
- Build plugin
- Externals and Shared Angular library
- Load plugin (client, server)
- How to render a plugin?
- How to isolate the main app from errors in a plugin?
Why Angular?
The Angular team and community are continuing the rapid growth of its ecosystem.
Angular helps us to structure our code in a consistent manner so every new developer can easily be involved in a project.
We like the fact that we’re following best web practices with Angular, just think of typescript, observables, server-side rendering, web workers, differential loading, progressive web application(PWA), lazy loading, etc. All of this helps us to adopt those features in a fast manner.
There are also more features that Angular offers us, like built-in dependency injection system, reactive forms, schematics and so on.
That is why we usually choose Angular when building an enterprise application.
The goal
One day our client asked us to add a new feature to his existing Angular Universal application. He wanted to have a pluggable Content Management System(CMS). The goal was to add the possibility to extend the functionality of the current app so that a 3rd party developer could easily develop a new module on his own and upload it. Then, the Angular app should pick it up without having to recompile the whole application and redeploy.
Simply put, we need to develop a plugin system.
What is a plugin?
Plugin systems allow an application to be extended without modification of the core application code.
It sounds simple but writing a plugin with Angular is always a challenge.
Why is it so hard to create a plugin with Angular?
One of my colleagues, who worked with AngularJS long time ago, said that he saw an Angular 2 application written in plain es5 (hmm.. maybe he found my jsfiddle or my old repo). So he suggested creating an Angular module in es5, putting it in a folder and that the main app should make it work somehow.
Don’t get me wrong but I’m with Angular since 2 alpha and Angular 2 (or just Angular) is a completely new thing.
Of course, the main pitfall here is Ahead Of Time (AOT) compilation. The main advantage of using AOT is better performance for your application.
I saw lots of examples out there that use JitCompiler for building a pluggable architecture. This is not the way we want to go. We have to keep our app fast and not include compiler code in the main bundle. That’s why we shouldn’t use es5 because only TypeScript code can be AOT precompiled. Also, the current implementation of Angular AOT is based on the @NgModule
transitive scope and requires that all should be compiled together. All of this makes things harder.
Another pitfall is that we need to share code between plugins to avoid code duplication. What kind of duplication can we consider? We distinguish two types of code that can be duplicated:
- The code that we write or the code that we take from
node_modules
- The code produced by AOT, think of component and module factories (
component.ngfactory.js
andmodule.ngfactory.js
). Actually, this is a huge amount of code.
In order to avoid these code duplications, we need to deal with the fact of how ViewEngine generates a factory.
If you don’t know, the ViewEngine is the current Angular rendering engine
The problem here is that code generated by the Angular compiler can point to ViewFactory from another piece of generated code. For instance, here’s how element definition is linked to ViewDefinitionFactory (Github source code)
elementDef(…, componentView?: null | ViewDefinitionFactory, componentRendererType?: RendererType2 | null)
So this results in getting duplicates of all the factories from the shared library.


So when we were discussing the Angular plugin system, we should keep the following in mind:
Requirements
- AOT
- Avoid duplicated code (packages like
@angular/core{common,forms,router},rxjs,tslib
) - Use a shared library in all plugins. But, DO NOT SHIP generated factories from that shared library in each plugin. Rather, reuse library code and factories.
- For importing the external modules we just need to know one thing: their bundle file path.
- Our code should recognize the module and place the plugin into the page.
- Support server-side rendering
- Load the module only when needed
- Support the same level of optimization that Angular CLI gives us
All of these considerations led us to our own solution.
Similar solutions
There are different approaches out there, but they lack the crucial parts: support for AOT, optimized code, and non-duplicated code.
And one of the solutions that is close to our needs is: https://github.com/iwnow/angular-plugin-example
It uses rollup to produce the plugin bundle in umd format.
However, here are the drawbacks I see with this approach:
- ❌ it doesn’t use the optimization techniques that Angular CLI offers us: i.e. it doesn’t remove Angular decorators and doesn’t run buildOptimizer.
- ❌ it duplicates factories if we use shared components in each plugin.
Towards a Solution
Fortunately, Angular is very extendable through custom scripts.
Since Angular 6, there’s the possibility to hook into the compilation process using builders. It allows us to add a custom webpack configuration, including all the benefits you can imagine from a vanilla webpack setup.
So I thought that it could be a good idea to write a custom builder for building plugins.
Angular CLI supports the generation of libraries. But ng-packagr, that is used to build that library, generates many artifacts that we don’t need. (Yeah, I know that it follows the Angular Package Format(APF)). And those artifacts can be only consumed by other Angular applications.
And here I thought that I could use an Angular application to build my plugins. By application, I mean an application which is generated by using a CLI command like ng generate application
. This approach is beneficial because it gives us the same level of optimization that Angular CLI does. For example, we won’t get Angular specific decorators after AOT.


Now, if only I could find a way to produce a single bundle with all exports I need:
Single bundleLink to this section
So, I started with:
<>Copyng generate application plugins —minimal
which gave me a simple new Angular application in projects
folder
<>Copyprojects |_ plugins src angular.json
Next, I removed all unnecessary files so that it looked like:
<>Copyprojects |_ plugins |_ src |_ plugin1 |_ plugin2 main.ts tsconfig.app.json src angular.json
Then I removed all the redundant stuff from angular.json
for that project:


As you can see, I removed index
, all assets
, scripts
, fileReplacements
etc. and I also created a custom PluginBuilder which is located in ./builders:plugin
.
The builder should produce a single umd bundle:
<>Copy// Make sure we are producing a single bundle delete config.entry.polyfills; delete config.optimization.runtimeChunk; delete config.optimization.splitChunks; delete config.entry.styles; config.output.library = pluginName; config.output.libraryTarget = 'umd';
Also, it should provide externals and generate the appropriate exports depending on which plugin we’re building. This is a vital part of the builder.
ExternalsLink to this section
Webpack allows us to provide externals. So they might look like:
<>Copyconfig.externals = { rxjs: 'rxjs', '@angular/core': 'ng.core', '@angular/common': 'ng.common', '@angular/forms': 'ng.forms', '@angular/router': 'ng.router', tslib: 'tslib' // put here other common dependencies };
There’s another option which I’ll show a bit later in the Shared library section.
Dynamic exportsLink to this section
Here is the interesting part which gives us the ability to build different plugins with the same build command.
First, thetsconfig.app.json
of theplugins
application only looks at the main.ts
file so that we only compile what we provide in main.ts
.
tsconfig.app.json
<>Copy{ "extends": "../../tsconfig.json", "compilerOptions": { ... }, "files": ["./src/main.ts"] }
Next, the builder takes the modulePath
parameter:
<>CopymodulePath=./plugin1/plugin1.module#Plugin1Module
You may notice that it looks like a path to a lazy module when we’re working on lazy loading. The builder analyzes that path and generates dedicated code in main.ts
. For example, in the modulePath
above we get:
<>Copyexport * from './plugin1/plugin1.module'; export * from './plugin1/plugin1.module.ngfactory'; import { Plugin1ModuleNgFactory } from './plugin1/plugin1.module.ngfactory'; export default Plugin1ModuleNgFactory;
The key point here is to be able to export not only the code, but also the factories generated by Angular compiler.
I keep main.ts
empty so that I can update it dynamically before the build and so it will export what we want.
Build pluginLink to this section
Here’s an example of the command I use to build the plugin:
<>Copy"build:plugin1": "ng build --project plugins --prod --modulePath=./plugin1/plugin1.module#Plugin1Module --pluginName=plugin1 --outputPath=./src/assets/plugins",
What is going here?
ng build — project plugins — prod
Build the above createdplugins
application in prod mode.modulePath=./plugin1/plugin1.module#Plugin1Module
Provide a path to the module we want to be a plugin.pluginName=plugin1
Define the name for the bundle.— outputPath=./src/assets/plugins
Specify the directory where the generated bundle should be placed.
Externals and Shared Angular librariesLink to this section
We also can use a shared library for plugins. To be able to expose all factories from the shared library we need to have the library code in one file. Otherwise, it would be hard to figure out what’s the path for those ngfactories.
And that’s what ng-packagr generates for us.
We can build generated by ng-packagr file. So, first, I create a library:
ng generate library shared
and then build it for the plugin system:
"build:shared": "ng build shared && ng build --project plugins --prod --modulePath=shared#SharedModule --pluginName=shared --outputPath=./src/assets/plugins",
Now, when I create a shared
library I can reuse it in my plugins:
"build:plugin1": "ng build --project plugins --prod --modulePath=./plugin1/plugin1.module#Plugin1Module --pluginName=plugin1 --sharedLibs=shared --outputPath=./src/assets/plugins",
Note--sharedLibs=shared
I’m controlling the factory externals via the webpack externals
options where I’m providing a special function to catch all ngfactory imports.


This way we cover all possible externals in our bundle:


And finally, get what we wanted:


Consuming the pluginLink to this section
First of all, we need to load our configuration from the server.
For the sake of simplicity, we load a simple JSON file, that looks like this:


Then, in order to consume the built plugin, we create two services for the client and server sides which extend PluginLoaderService
abstract class:


Client Side
We use Angular Dependency Injection to provide a client specific loader only for the client code:
app.module.ts
<>Copyproviders: [ { provide: PluginLoaderService, useClass: ClientPluginLoaderService } ],
In order to load the plugin on the client-side, we use a minimal systemjs@3.0.2
build that supports loading AMD modules.
<>Copy"scripts": [ "node_modules/systemjs/dist/s.js", "node_modules/systemjs/dist/extras/named-register.js", "node_modules/systemjs/dist/extras/amd.js" ],
Externals are provided by the define
global function:
<>CopyObject.keys(PLUGIN_EXTERNALS_MAP).forEach(externalKey => window.define(externalKey, [], () => PLUGIN_EXTERNALS_MAP[externalKey]) );
And the load function looks like
<>Copyload<T>(pluginName): Promise<NgModuleFactory<T>> { return SystemJs.import(config[pluginName].path).then( module => module.default.default ); }
Server SideLink to this section
The Server code has its own implementation of the loader service:
app.server.module.ts
<>Copyproviders: [ { provide: PluginLoaderService, useClass: ServerPluginLoaderService } ],
The service uses the nodejs require method to get the module:
<>Copyload<T>(pluginName): Promise<NgModuleFactory<T>> { const factory = global['require'](`./browser${config[pluginName].path}`) .default; return Promise.resolve(factory); }
We also override the native require
method in order to provide externals:


The case when the main application uses the same shared components as pluginsLink to this section
Let’s now imagine that our shared library should be also used in our main application:


What can we do to not duplicate the code?
Since we have theshared
library already built in the main app we can define it as externals to other plugins.
plugin-externals.prod.ts
<>Copyimport * as shared from 'shared'; export const PLUGIN_EXTERNALS_MAP = { 'ng.core': core, 'ng.common': common, 'ng.forms': forms, 'ng.router': router, rxjs, tslib, shared: { ...shared, ...require('shared/shared.ngfactory') } };
This way we share both the library code and the code produced by aot.
You may note that I have prod
postfix in the file above. That’s because we define such external only for production build.
<>Copy"configurations": { "production": { "fileReplacements": [ ... { "replace": "src/app/services/plugin-loader/plugin-externals.ts", "with": "src/app/services/plugin-loader/plugin-externals.prod.ts" } ],
In dev mode we still load shared
library through Systemjs as other plugins.
An example of the code for this case can be found in share-lib-between-app-and-plugins branch.
How do we render the plugin?Link to this section
Now that we have all we need to get NgModuleFactory, we can render our plugins with the help of one of the well-known Angular API methods for creating a dynamic component:
componentFactory.create
viewContainerRef.createComponent
In the example below, we used the second option:


Isolating the main app from errors in a pluginLink to this section
One of the problems with a pluggable architecture is that exceptions in plugins can sink the entire app.
Fortunately, there are a few different ways for dealing with such errors:
- Global ErrorHandler
- Zone.onError.subscribe
- try catch
This lets us gracefully handle any errors when creating a plugin, and safely isolate, kill, and report bad components without impacting the rest of our app.
SummaryLink to this section
Angular evolves each quarter. But, it still doesn’t have a simple way to deal with dynamic templating. It’s the most required part when we are working with a big application and clients demand that all should be dynamic, lazy and changeable at runtime.
Luckily, Angular CLI provides us the tools to configure a build for our specific needs. We showed in this article one option for how to build an AOT precompiled plugin. It might not exactly work for your needs, but possibly you can borrow some of the ideas in this article. So, I hope you found them useful.
Thank you for reading!
The code can be found on Github: angular-plugin-architecture.
Comments (0)
Be the first to leave a comment
About the author

Alexey is a GDE for Angular and Web Technologies and also active StackOverflow contributor.

About the author
Alexey Zuev
Alexey is a GDE for Angular and Web Technologies and also active StackOverflow contributor.
About the author

Alexey is a GDE for Angular and Web Technologies and also active StackOverflow contributor.