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 and module.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.

Duplicates of ngFactories in non-optimized plugin

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.