# Micro-frontends with Module Federation

Module Federation enables independent deployment and development of micro-frontends by allowing different Aurelia applications to share code and components at runtime. This guide covers implementing Module Federation with both Webpack 5 and Vite for maximum flexibility.

## Understanding Module Federation

Module Federation allows you to:

* **Share components** between Aurelia applications at runtime
* **Deploy independently** without coordinating releases
* **Load modules dynamically** from remote applications
* **Avoid code duplication** across micro-frontends
* **Mix technology stacks** (Aurelia with React, Vue, etc.)

## Architecture Overview

{% @mermaid/diagram content="graph TB
Shell\["🏠 Shell App<br/>(Host)<br/><br/>• Router<br/>• Navigation<br/>• Shared UI"]
Product\["📦 Product App<br/>(Remote)<br/><br/>• ProductList<br/>• ProductDetail"]
User\["👤 User App<br/>(Remote)<br/><br/>• UserProfile<br/>• UserSettings"]

```
Shell <--> Product
Shell --> User

classDef hostApp fill:#e1f5fe,stroke:#01579b,stroke-width:3px,color:#000
classDef remoteApp fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000

class Shell hostApp
class Product,User remoteApp" %}
```

## Webpack 5 Module Federation

### 1. Install Dependencies

```bash
npm install webpack@5 webpack-cli webpack-dev-server html-webpack-plugin
npm install aurelia ts-loader html-loader
```

### 2. Configure the Remote Application (Product App)

**webpack.config.js:**

```javascript
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  devServer: {
    port: 4001,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.html$/i,
        use: 'html-loader',
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'productApp',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/product-list',
        './ProductDetail': './src/components/product-detail',
        './ProductModule': './src/product-module',
      },
      shared: {
        aurelia: {
          singleton: true,
          requiredVersion: '^2.0.0',
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
  resolve: {
    extensions: ['.ts', '.js'],
  },
};
```

### 3. Create Exposed Components

**src/components/product-list.ts:**

```typescript
import { customElement, bindable } from '@aurelia/runtime-html';

export interface Product {
  id: number;
  name: string;
  price: number;
}

@customElement({
  name: 'product-list',
  template: `
    <div class="product-grid">
      <div class="product-card" repeat.for="product of products">
        <h3>\${product.name}</h3>
        <p>$\${product.price}</p>
        <button click.trigger="selectProduct(product)">
          View Details
        </button>
      </div>
    </div>
  `
})
export class ProductList {
  @bindable public products: Product[] = [];
  @bindable public onProductSelect?: (product: Product) => void;

  public selectProduct(product: Product): void {
    this.onProductSelect?.(product);
  }
}
```

**src/product-module.ts:**

```typescript
import { IContainer, IRegistry } from '@aurelia/kernel';
import { ProductList } from './components/product-list';
import { ProductDetail } from './components/product-detail';

export const ProductModule: IRegistry = {
  register(container: IContainer): void {
    container.register(ProductList, ProductDetail);
  }
};
```

### 4. Configure the Host Application (Shell App)

**webpack.config.js:**

```javascript
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  devServer: {
    port: 4000,
    historyApiFallback: true,
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.html$/i,
        use: 'html-loader',
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        productApp: 'productApp@http://localhost:4001/remoteEntry.js',
        userApp: 'userApp@http://localhost:4002/remoteEntry.js',
      },
      shared: {
        aurelia: {
          singleton: true,
          requiredVersion: '^2.0.0',
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
  resolve: {
    extensions: ['.ts', '.js'],
  },
};
```

### 5. Dynamic Loading in Host Application

**src/components/micro-frontend-loader.ts:**

```typescript
import { customElement, bindable } from '@aurelia/runtime-html';
import { IContainer } from '@aurelia/kernel';

@customElement({
  name: 'micro-frontend-loader',
  template: `
    <div if.bind="loading">Loading micro-frontend...</div>
    <div if.bind="error" class="error">
      Failed to load micro-frontend: \${error}
    </div>
    <div if.bind="!loading && !error" ref="container"></div>
  `
})
export class MicroFrontendLoader {
  @bindable public remoteName: string = '';
  @bindable public moduleName: string = '';
  @bindable public componentName: string = '';

  private container!: HTMLElement;
  private loading = false;
  private error: string | null = null;

  constructor(private aurelia: IContainer) {}

  public async attached(): Promise<void> {
    if (!this.remoteName || !this.moduleName) {
      this.error = 'Remote name and module name are required';
      return;
    }

    await this.loadMicroFrontend();
  }

  private async loadMicroFrontend(): Promise<void> {
    this.loading = true;
    this.error = null;

    try {
      // Dynamic import from remote
      const remoteModule = await import(
        `${this.remoteName}/${this.moduleName}`
      );

      if (this.componentName) {
        // Load specific component
        const ComponentClass = remoteModule[this.componentName];
        if (ComponentClass) {
          // Register and render component
          this.aurelia.register(ComponentClass);
          // Custom rendering logic here
        }
      } else {
        // Load entire module registry
        const moduleRegistry = remoteModule.default || remoteModule[this.moduleName];
        if (moduleRegistry && typeof moduleRegistry.register === 'function') {
          moduleRegistry.register(this.aurelia);
        }
      }
    } catch (err) {
      console.error('Failed to load micro-frontend:', err);
      this.error = err instanceof Error ? err.message : 'Unknown error';
    } finally {
      this.loading = false;
    }
  }
}
```

**src/my-app.ts:**

```typescript
import { route } from '@aurelia/router';

@route({
  routes: [
    { path: '', redirectTo: 'home' },
    { path: 'home', component: () => import('./views/home') },
    {
      path: 'products',
      component: () => import('./views/products-shell'),
      title: 'Products'
    },
    {
      path: 'users',
      component: () => import('./views/users-shell'),
      title: 'Users'
    }
  ]
})
export class MyApp {
  public message = 'Shell Application';
}
```

**src/views/products-shell.html:**

```html
<div class="products-page">
  <h2>Products (Micro-frontend)</h2>
  <micro-frontend-loader
    remote-name="productApp"
    module-name="ProductList"
    component-name="ProductList">
  </micro-frontend-loader>
</div>
```

## Vite Module Federation

### 1. Install Dependencies

```bash
npm install vite @vitejs/plugin-legacy
npm install @originjs/vite-plugin-federation
# Alternative: npm install @module-federation/vite
```

### 2. Configure Remote Application

**vite.config.ts:**

```typescript
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    federation({
      name: 'productApp',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/product-list.ts',
        './ProductModule': './src/product-module.ts',
      },
      shared: {
        aurelia: {
          singleton: true,
        },
      },
    }),
  ],
  server: {
    port: 4001,
    cors: true,
  },
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});
```

### 3. Configure Host Application

**vite.config.ts:**

```typescript
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    federation({
      name: 'shell',
      remotes: {
        productApp: 'http://localhost:4001/assets/remoteEntry.js',
        userApp: 'http://localhost:4002/assets/remoteEntry.js',
      },
      shared: {
        aurelia: {
          singleton: true,
        },
      },
    }),
  ],
  server: {
    port: 4000,
  },
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});
```

## Advanced Patterns

### Error Boundaries and Fallbacks

**src/components/micro-frontend-boundary.ts:**

```typescript
import { customElement, bindable } from '@aurelia/runtime-html';

@customElement({
  name: 'micro-frontend-boundary',
  template: `
    <div if.bind="hasError" class="error-boundary">
      <h3>Something went wrong</h3>
      <p>\${errorMessage}</p>
      <button click.trigger="retry()">Retry</button>
    </div>
    <div if.bind="!hasError">
      <slot></slot>
    </div>
  `
})
export class MicroFrontendBoundary {
  @bindable public onError?: (error: Error) => void;

  private hasError = false;
  private errorMessage = '';

  public handleError(error: Error): void {
    this.hasError = true;
    this.errorMessage = error.message;
    this.onError?.(error);
    console.error('Micro-frontend error:', error);
  }

  public retry(): void {
    this.hasError = false;
    this.errorMessage = '';
    // Trigger re-render of child content
  }
}
```

### Shared State Management

**src/services/micro-frontend-state.ts:**

```typescript
import { IEventAggregator, singleton } from '@aurelia/kernel';

export interface MicroFrontendMessage {
  source: string;
  type: string;
  payload: any;
}

@singleton()
export class MicroFrontendState {
  constructor(private eventAggregator: IEventAggregator) {}

  public publish(message: MicroFrontendMessage): void {
    this.eventAggregator.publish('micro-frontend:message', message);
  }

  public subscribe(callback: (message: MicroFrontendMessage) => void): void {
    this.eventAggregator.subscribe('micro-frontend:message', callback);
  }

  public shareData(key: string, data: any): void {
    (window as any).__SHARED_STATE__ = (window as any).__SHARED_STATE__ || {};
    (window as any).__SHARED_STATE__[key] = data;
  }

  public getData(key: string): any {
    return (window as any).__SHARED_STATE__?.[key];
  }
}
```

## Performance Optimizations

### Preloading Remote Modules

```typescript
// Preload critical micro-frontends
const preloadModules = async () => {
  try {
    // Preload but don't execute
    await import(/* webpackPreload: true */ 'productApp/ProductModule');
  } catch (error) {
    console.warn('Failed to preload module:', error);
  }
};

// Call during app initialization
preloadModules();
```

### Lazy Loading Strategy

```typescript
import { customElement, bindable } from '@aurelia/runtime-html';

@customElement({
  name: 'lazy-micro-frontend',
  template: `
    <div class="intersection-observer" ref="trigger">
      <div if.bind="visible">
        <micro-frontend-loader
          remote-name.bind="remoteName"
          module-name.bind="moduleName">
        </micro-frontend-loader>
      </div>
    </div>
  `
})
export class LazyMicroFrontend {
  @bindable public remoteName: string = '';
  @bindable public moduleName: string = '';

  private trigger!: HTMLElement;
  private visible = false;

  public attached(): void {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        this.visible = true;
        observer.disconnect();
      }
    });

    observer.observe(this.trigger);
  }
}
```

## Best Practices

### 1. Versioning Strategy

* Use semantic versioning for shared dependencies
* Pin major versions to avoid breaking changes
* Test compatibility across micro-frontends

### 2. Development Workflow

```bash
# Start all micro-frontends in development
npm run dev:shell    # Port 4000
npm run dev:products # Port 4001
npm run dev:users    # Port 4002
```

### 3. Production Deployment

* Deploy each micro-frontend independently
* Use CDN for shared dependencies
* Implement health checks for remote modules
* Set up monitoring for failed module loads

### 4. Testing Strategy

* Unit test components in isolation
* Integration test the shell application
* E2E test critical user journeys
* Contract testing between micro-frontends

This Module Federation setup enables scalable micro-frontend architectures with Aurelia 2, supporting both Webpack 5 and Vite build systems while maintaining independent development and deployment capabilities.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aurelia.io/tutorials/micro-frontends-with-module-federation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
