npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@klerick/nx-angular-mf

v21.0.0

Published

Custom Angular Builder for Microfrontend Architecture using Native ESM and Import Maps.

Readme

@klerick/nx-angular-mf

Custom Angular Builder for Microfrontend Architecture using Native ESM and Import Maps.

Table of Contents

Overview

This builder enables Angular microfrontend architecture using native ES modules and Import Maps. Unlike Webpack Module Federation, this approach uses browser-native module resolution without runtime overhead.

Key Features

  • Native ESM - No Webpack runtime, pure ES modules
  • Import Maps - Browser-native module resolution
  • Shared Dependencies - Single instance of Angular, RxJS, etc.
  • SSR Support - Full server-side rendering with custom loaders
  • CDN Ready - Designed for CDN deployment

Architecture

System Overview (Without SSR)

┌─────────────────────────────────────────────────────────────────────┐
│                              CDN                                     │
│                                                                      │
│  ┌─────────────────────┐      ┌─────────────────────┐              │
│  │   Host Application   │      │   MF1 Application   │              │
│  │                      │      │                      │              │
│  │  - main.js           │      │  - FirstRemoteRoute.js│             │
│  │  - index.html        │      │  - SecondRemoteRoute.js│            │
│  │  - _angular_core.js  │      │  - import-map-config.json│          │
│  │  - _angular_router.js│      │                      │              │
│  │  - _rxjs.js          │      └─────────────────────┘              │
│  │  - import-map-config.json│                                        │
│  └─────────────────────┘                                            │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                            Browser                                   │
│                                                                      │
│  1. Load index.html from Host                                       │
│  2. Parse Import Map                                                │
│  3. Load main.js                                                    │
│  4. When navigating to MF route:                                    │
│     - Import Map resolves 'mf1/FirstRemoteRoute'                    │
│     - Browser fetches from MF1 CDN URL                              │
│     - MF1 uses shared deps from Host (via Import Map)               │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Component Diagram

┌──────────────────────────────────────────────────────────────────┐
│                        Host Application                           │
│                                                                   │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐  │
│  │   Router    │───▶│   Routes    │───▶│  loadModule()       │  │
│  └─────────────┘    └─────────────┘    │  'mf1/FirstRoute'   │  │
│                                         └──────────┬──────────┘  │
│                                                     │             │
│  ┌─────────────────────────────────────────────────┼───────────┐ │
│  │                    Import Map                    │           │ │
│  │                                                  ▼           │ │
│  │  "imports": {                                               │ │
│  │    "@angular/core": "/host/_angular_core.js",               │ │
│  │    "mf1/FirstRoute": "https://cdn/mf1/FirstRemoteRoute.js"  │ │
│  │  }                                                          │ │
│  └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
                                    │
                                    │ HTTP Request
                                    ▼
┌──────────────────────────────────────────────────────────────────┐
│                      MF1 Application (CDN)                        │
│                                                                   │
│  ┌─────────────────────┐                                         │
│  │ FirstRemoteRoute.js │                                         │
│  │                     │                                         │
│  │ import { Component }│──────┐                                  │
│  │ from '@angular/core'│      │                                  │
│  │                     │      │ Resolved via Host's Import Map   │
│  └─────────────────────┘      │ to: /host/_angular_core.js       │
│                               │                                   │
│                               ▼                                   │
│                    Uses SHARED @angular/core                      │
│                    (same instance as Host)                        │
└──────────────────────────────────────────────────────────────────┘

How It Works

1. Shared Dependencies as Entry Points

The builder extracts shared dependencies (Angular, RxJS, etc.) as separate entry points:

Input:                          Output:
@angular/core          →        _angular_core.js
@angular/router        →        _angular_router.js
rxjs                   →        rxjs.js

Each dependency becomes a standalone ES module that can be shared across all applications.

2. Import Map Generation

During build, an import-map-config.json is generated:

{
  "imports": {
    "@angular/core": "https://cdn.example.com/_angular_core-HASH.js",
    "@angular/router": "https://cdn.example.com/_angular_router-HASH.js",
    "rxjs": "https://cdn.example.com/rxjs-HASH.js"
  },
  "exposes": {
    "FirstRemoteRoute": "./FirstRemoteRoute-HASH.js"
  },
  "remoteEntry": {}
}

3. HTML Transformation

The builder transforms index.html to:

  1. Inject <script type="importmap"> before module scripts
  2. Fix <link rel="modulepreload"> URLs
  3. Move module scripts after import map
<body>
  <app-root></app-root>

  <!-- Import Map (injected by builder) -->
  <script type="importmap">
  {
    "imports": {
      "@angular/core": "https://cdn.example.com/_angular_core.js",
      "mf1/FirstRemoteRoute": "https://mf1.cdn.com/FirstRemoteRoute.js"
    }
  }
  </script>

  <!-- Modulepreload for faster loading -->
  <link rel="modulepreload" href="https://cdn.example.com/_angular_core.js">

  <!-- Application entry point -->
  <script type="module" src="main.js"></script>
</body>

Build Process

MF Application Build

┌─────────────────────────────────────────────────────────────────┐
│                    MF Build Process                              │
│                                                                  │
│  Input:                                                          │
│  ┌──────────────────┐                                           │
│  │ mf.exposes:      │                                           │
│  │   FirstRoute     │                                           │
│  │   SecondRoute    │                                           │
│  │                  │                                           │
│  │ mf.externalList: │                                           │
│  │   @angular/core  │                                           │
│  │   @angular/router│                                           │
│  └────────┬─────────┘                                           │
│           │                                                      │
│           ▼                                                      │
│  ┌──────────────────┐                                           │
│  │  esbuild         │                                           │
│  │  + plugins       │                                           │
│  └────────┬─────────┘                                           │
│           │                                                      │
│           ▼                                                      │
│  Output:                                                         │
│  ┌──────────────────┐                                           │
│  │ dist/browser/    │                                           │
│  │  ├─ FirstRemoteRoute-HASH.js   (exposed module)             │
│  │  ├─ SecondRemoteRoute-HASH.js  (exposed module)             │
│  │  ├─ _angular_core-HASH.js      (shared dep)                 │
│  │  ├─ _angular_router-HASH.js    (shared dep)                 │
│  │  ├─ import-map-config.json     (metadata)                   │
│  │  └─ index.html                                               │
│  └──────────────────┘                                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Host Application Build

┌─────────────────────────────────────────────────────────────────┐
│                   Host Build Process                             │
│                                                                  │
│  Input:                                                          │
│  ┌──────────────────┐                                           │
│  │ mf.remoteEntry:  │                                           │
│  │   mf1: https://..│                                           │
│  │                  │                                           │
│  │ mf.externalList: │                                           │
│  │   @angular/core  │                                           │
│  │   @angular/router│                                           │
│  └────────┬─────────┘                                           │
│           │                                                      │
│           ▼                                                      │
│  ┌──────────────────────────────────────────────────┐           │
│  │  1. Build application                            │           │
│  │  2. Build shared dependencies as entry points    │           │
│  │  3. Fetch import-map-config.json from each MF    │           │
│  │  4. Merge into final import map                  │           │
│  │  5. Transform index.html                         │           │
│  └────────┬─────────────────────────────────────────┘           │
│           │                                                      │
│           ▼                                                      │
│  Output:                                                         │
│  ┌──────────────────┐                                           │
│  │ dist/browser/    │                                           │
│  │  ├─ main-HASH.js                                            │
│  │  ├─ _angular_core-HASH.js                                   │
│  │  ├─ _angular_router-HASH.js                                 │
│  │  ├─ import-map-config.json                                  │
│  │  └─ index.html (with merged import map)                     │
│  └──────────────────┘                                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Deploy Flow

Recommended CDN Deployment

Step 1: Build & Deploy MF
─────────────────────────

  Developer          CI/CD              CDN (MF1)
      │                │                    │
      │  git push      │                    │
      │───────────────▶│                    │
      │                │                    │
      │                │  nx build mf1      │
      │                │  (production)      │
      │                │                    │
      │                │  Deploy artifacts  │
      │                │───────────────────▶│
      │                │                    │
      │                │    ┌───────────────┴───────────────┐
      │                │    │  https://mf1.cdn.com/         │
      │                │    │  ├─ FirstRemoteRoute-ABC.js   │
      │                │    │  ├─ import-map-config.json    │
      │                │    │  └─ ...                       │
      │                │    └───────────────────────────────┘


Step 2: Build & Deploy Host
───────────────────────────

  Developer          CI/CD              CDN (Host)
      │                │                    │
      │  git push      │                    │
      │───────────────▶│                    │
      │                │                    │
      │                │  1. nx build host  │
      │                │                    │
      │                │  2. Fetch MF1's    │
      │                │     import-map-config.json
      │                │     from CDN       │
      │                │                    │
      │                │  3. Merge import   │
      │                │     maps           │
      │                │                    │
      │                │  4. Deploy         │
      │                │───────────────────▶│
      │                │                    │
      │                │    ┌───────────────┴───────────────┐
      │                │    │  https://host.cdn.com/        │
      │                │    │  ├─ index.html                │
      │                │    │  │   (contains merged         │
      │                │    │  │    import map)             │
      │                │    │  ├─ main-XYZ.js               │
      │                │    │  ├─ _angular_core-XYZ.js      │
      │                │    │  └─ ...                       │
      │                │    └───────────────────────────────┘

Import Map Merging

When Host builds, it fetches import-map-config.json from each remote:

Host's import-map-config.json:
{
  "imports": {
    "@angular/core": "https://host.cdn.com/_angular_core-XYZ.js"
  }
}

+

MF1's import-map-config.json (fetched from CDN):
{
  "imports": { ... },
  "exposes": {
    "FirstRemoteRoute": "./FirstRemoteRoute-ABC.js"
  }
}

=

Final Import Map in index.html:
{
  "imports": {
    "@angular/core": "https://host.cdn.com/_angular_core-XYZ.js",
    "mf1/FirstRemoteRoute": "https://mf1.cdn.com/FirstRemoteRoute-ABC.js"
  },
  "scopes": {
    "https://mf1.cdn.com/": {
      "@angular/core": "https://host.cdn.com/_angular_core-XYZ.js"
    }
  }
}

Why Scopes?

Scopes ensure that when MF1's code imports @angular/core, it resolves to the Host's version, not its own:

// Inside FirstRemoteRoute-ABC.js (on mf1.cdn.com)
import { Component } from '@angular/core';

// Browser sees the scope "https://mf1.cdn.com/"
// Resolves @angular/core to https://host.cdn.com/_angular_core-XYZ.js
// Result: SHARED singleton Angular instance

Import Maps

Structure

{
  "imports": {
    // Global mappings - used by default
    "@angular/core": "https://cdn.com/_angular_core.js",
    "mf1/FirstRoute": "https://mf1.cdn.com/FirstRoute.js"
  },
  "scopes": {
    // Scoped mappings - used when importing from specific URLs
    "https://mf1.cdn.com/": {
      "@angular/core": "https://cdn.com/_angular_core.js"
    }
  }
}

Resolution Flow

Code Location                Import Statement           Resolved URL
─────────────────────────────────────────────────────────────────────
Host (cdn.com)              @angular/core         →   cdn.com/_angular_core.js
MF1 (mf1.cdn.com)           @angular/core         →   cdn.com/_angular_core.js (via scope)
Host (cdn.com)              mf1/FirstRoute        →   mf1.cdn.com/FirstRoute.js

Installation

npm install @klerick/nx-angular-mf

Prerequisites

  • Node.js v20+
  • Angular v21+
  • NX v20+

Configuration

project.json

{
  "name": "my-app",
  "targets": {
    "build": {
      "executor": "@klerick/nx-angular-mf:build",
      "options": {
        "outputPath": "dist/apps/my-app",
        "index": "apps/my-app/src/index.html",
        "browser": "apps/my-app/src/main.ts",
        "mf": {
          "externalList": "build-external-list.json",
          "skipList": "build-skip-list.json",
          "exposes": {
            "MyComponent": "./src/app/my-component/index.ts"
          },
          "remoteEntry": {
            "mf1": "https://mf1.cdn.com/"
          },
          "deployUrlEnvName": "DEPLOY_URL"
        }
      }
    },
    "serve": {
      "executor": "@klerick/nx-angular-mf:serve",
      "options": {
        "port": 4200
      }
    }
  }
}

Configuration Options

| Option | Type | Description | |--------|------|-------------| | externalList | string \| string[] | Dependencies to treat as external (loaded via import map) | | skipList | string \| string[] | Dependencies to exclude from processing | | exposes | Record<string, string> | Modules to expose for remote consumption | | remoteEntry | Record<string, string> | Remote application URLs | | esPlugins | string[] | Custom esbuild plugins | | indexHtmlTransformer | string | Custom HTML transformer | | deployUrlEnvName | string | Environment variable for deploy URL |

External List Example

build-external-list.json:

[
  "@angular/core",
  "@angular/common",
  "@angular/router",
  "@angular/forms",
  "@angular/platform-browser",
  "rxjs"
]

Usage Examples

Loading Remote Routes

import { Route } from '@angular/router';
import { loadModule } from '@klerick/nx-angular-mf/loadModule';

export const routes: Route[] = [
  {
    path: '',
    loadComponent: () => import('./home.component').then(m => m.HomeComponent)
  },
  {
    path: 'remote',
    loadChildren: () =>
      loadModule<{ routes: Route[] }>('mf1/FirstRemoteRoute')
        .then(m => m.routes)
  }
];

Loading Remote Components

import { Component, ViewContainerRef, inject } from '@angular/core';
import { loadModule } from '@klerick/nx-angular-mf/loadModule';

@Component({
  selector: 'app-container',
  template: '<ng-container #container></ng-container>'
})
export class ContainerComponent {
  private vcr = inject(ViewContainerRef);

  async loadRemote() {
    const { RemoteComponent } = await loadModule('mf1/RemoteComponent');
    this.vcr.createComponent(RemoteComponent);
  }
}

SSR Support

Why So Few Files in /server?

After building with SSR, the /server folder contains only:

dist/apps/host-application/
├── browser/                          # Browser bundle
│   ├── _angular_core-HASH.js         # Shared dependencies
│   ├── _angular_router-HASH.js
│   ├── _angular_common-HASH.js
│   ├── rxjs-HASH.js
│   ├── main-HASH.js
│   ├── import-map-config.json
│   └── index.html
│
└── server/                           # Server bundle
    ├── server.ssr.mjs                # Entry point (bootstrap)
    ├── server.mjs                    # Express/Fastify server
    ├── main.server.mjs               # Angular SSR app
    ├── custom-loader.mjs             # Node.js loader hooks
    ├── angular-app-manifest.mjs
    └── index.server.html

Why no _angular_core.js in server folder?

The server uses Node.js custom loader hooks to load shared dependencies from the /browser folder at runtime. This avoids duplicating large files and ensures the same code runs on both client and server.

How SSR Works

┌────────────────────────────────────────────────────────────────────────┐
│                          SSR Flow                                       │
│                                                                         │
│  1. Node.js starts with custom loader                                  │
│     node --import ./server.ssr.mjs                                     │
│                                                                         │
│  2. server.ssr.mjs registers custom-loader.mjs                         │
│     ┌─────────────────────────────────────────────────────────────┐   │
│     │  import { register } from 'module';                         │   │
│     │  register('./custom-loader.mjs', { ... });                  │   │
│     │  port.postMessage({ kind: 'DEPLOY_URL', result: '...' });   │   │
│     │  import('./server.mjs');                                     │   │
│     └─────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  3. Custom loader fetches import-map-config.json                       │
│     GET http://localhost:4200/import-map-config.json                   │
│                                                                         │
│  4. When server imports '@angular/core':                               │
│     ┌─────────────────────────────────────────────────────────────┐   │
│     │  resolve('@angular/core')                                   │   │
│     │      ↓                                                      │   │
│     │  Import Map: "@angular/core" → "http://host/_angular_core.js" │   │
│     │      ↓                                                      │   │
│     │  load() fetches content via HTTP                            │   │
│     │      ↓                                                      │   │
│     │  Returns: { format: 'module', source: '...' }               │   │
│     └─────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  5. Server renders HTML with shared Angular instance                   │
│                                                                         │
└────────────────────────────────────────────────────────────────────────┘

Custom Loader Architecture

Node.js doesn't natively support Import Maps. The custom loader provides three hooks:

┌──────────────────────────────────────────────────────────────────┐
│                    Node.js Loader Hooks                           │
│                                                                   │
│  ┌─────────────┐                                                 │
│  │ initialize  │  Receives DEPLOY_URL via MessagePort            │
│  │             │  Fetches import-map-config.json                 │
│  └──────┬──────┘                                                 │
│         │                                                         │
│         ▼                                                         │
│  ┌─────────────┐                                                 │
│  │  resolve    │  Maps specifiers using Import Map               │
│  │             │                                                  │
│  │  '@angular/core'  →  'http://host/_angular_core.js'          │
│  │  'mf1/Route'      →  'http://mf1/_Route.js'                  │
│  └──────┬──────┘                                                 │
│         │                                                         │
│         ▼                                                         │
│  ┌─────────────┐                                                 │
│  │    load     │  Fetches HTTP URLs and returns source           │
│  │             │                                                  │
│  │  fetch('http://host/_angular_core.js')                        │
│  │  return { format: 'module', source: content }                 │
│  └─────────────┘                                                 │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Production vs Development

Production:

┌──────────────────────────────────────────────────────────────────┐
│                    Production SSR                                 │
│                                                                   │
│  Server (Node.js)              CDN / Static Server               │
│  ┌────────────────┐            ┌────────────────────┐           │
│  │ server.ssr.mjs │──fetch────▶│ /browser/          │           │
│  │                │            │  _angular_core.js  │           │
│  │ custom-loader  │            │  _angular_router.js│           │
│  │                │            │  import-map-config │           │
│  └────────────────┘            └────────────────────┘           │
│                                                                   │
│  Loads shared deps from CDN at runtime                           │
│  Same files served to both browser and server                    │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Development (with Vite):

┌──────────────────────────────────────────────────────────────────┐
│                    Development SSR                                │
│                                                                   │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │                   Vite Dev Server                           │  │
│  │                                                             │  │
│  │  Browser Request          SSR Request                       │  │
│  │       │                        │                            │  │
│  │       ▼                        ▼                            │  │
│  │  Vite transforms          Custom Loader                     │  │
│  │  ES modules on-the-fly    intercepts imports                │  │
│  │                                │                            │  │
│  │       │                        │                            │  │
│  │       ▼                        ▼                            │  │
│  │  ┌───────────────────────────────────────────────────┐     │  │
│  │  │              In-Memory File Cache                  │     │  │
│  │  │   (shared between browser and SSR)                 │     │  │
│  │  └───────────────────────────────────────────────────┘     │  │
│  │                                                             │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                   │
│  patch-vite-dev-server.ts adds PREF prefix to bypass Vite        │
│  custom-loader-serve.ts handles SSR module resolution            │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

Key Implementation Details

  1. server.ssr.mjs - Bootstrap entry:

    import { register } from 'module';
    const { port1, port2 } = new MessageChannel();
    register('./custom-loader.mjs', { data: { port: port2 }, transferList: [port2] });
    port1.postMessage({ kind: 'DEPLOY_URL', result: 'http://localhost:4200/' });
    import('./server.mjs');
  2. custom-loader.mjs - Module resolution:

    • initialize() - receives DEPLOY_URL, fetches import-map-config.json
    • resolve() - maps bare specifiers to URLs via Import Map
    • load() - fetches HTTP URLs and returns ESM source
  3. ngServerMode flag:

    // Injected before each module loaded via HTTP
    var ngServerMode = true;
    // ... module source code

    This tells Angular the code is running in SSR context.

Remote MF Loading in SSR

When server needs to render a remote microfrontend route:

┌──────────────────────────────────────────────────────────────────┐
│                Remote MF SSR Resolution                           │
│                                                                   │
│  1. Server imports 'mf1/FirstRemoteRoute'                        │
│                                                                   │
│  2. Custom loader resolves via Import Map:                       │
│     'mf1/FirstRemoteRoute' → 'http://mf1.cdn.com/FirstRoute.js'  │
│                                                                   │
│  3. Loader checks scope in Import Map:                           │
│     scope: 'http://mf1.cdn.com/' exists                          │
│                                                                   │
│  4. Fetches MF1's import-map-config.json                         │
│     GET http://mf1.cdn.com/import-map-config.json                │
│                                                                   │
│  5. Merges MF1's dependencies with host scope:                   │
│     If MF1 has different '@angular/core' URL,                    │
│     it's added to scope for that origin                          │
│                                                                   │
│  6. Fetches and returns FirstRemoteRoute.js                      │
│     with ngServerMode = true injected                            │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

SSR Configuration

{
  "build": {
    "executor": "@klerick/nx-angular-mf:build",
    "options": {
      "server": "apps/my-app/src/main.server.ts",
      "ssr": {
        "entry": "apps/my-app/src/server.ts"
      },
      "outputMode": "server"
    }
  }
}

Benefits of This Approach

| Benefit | Description | |---------|-------------| | No Duplication | Shared deps stored once in /browser, loaded at runtime | | Same Code | Identical modules run on client and server | | Dynamic Loading | Remote MFs fetched on-demand during SSR | | CDN Ready | Server can load from any HTTP endpoint | | Hot Reload | Dev server shares cache between browser and SSR |

Important Notes

Version Compatibility

All applications sharing dependencies must use compatible versions:

✅ Host: @angular/[email protected]
   MF1:  @angular/[email protected]  (same hash, same file)

❌ Host: @angular/[email protected] (without zone.js)
   MF1:  @angular/[email protected] (with zone.js)
   → Different builds, different hashes!

Zone.js Consideration

Angular 21+ supports zoneless mode. All apps must use the same mode:

// All apps must match:
"polyfills": []           // zoneless
// OR
"polyfills": ["zone.js"]  // with zone.js

Different zone.js configurations produce different builds of @angular/core.

License

MIT