Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
142638b
feat: add micro-frontend example using @module-federation/vite (close…
chrisjwalk-bot May 30, 2026
12ad4fe
fix: normalize server.watch for @module-federation/vite Vite 8 compat…
chrisjwalk-bot May 30, 2026
ac0d20a
feat: auto-start counter-remote when serving web-app
chrisjwalk-bot May 30, 2026
137a9a1
fix(counter-remote): stub virtual:pwa-register for dev server
chrisjwalk-bot May 30, 2026
0af2975
fix: share @ngrx/signals as singleton in MFE config to resolve NG0200
chrisjwalk-bot May 30, 2026
c4ce927
fix(mfe): share Angular CDK, Material, and workspace libs as singletons
chrisjwalk-bot May 31, 2026
694295e
fix(mfe): remove workspace libs from shared (MF virtual module limita…
chrisjwalk-bot May 31, 2026
29c7e37
fix(mfe): add missing material/divider and material/sidenav to MF sha…
chrisjwalk-bot May 31, 2026
8c29273
feat: mfe example with counter remote (closes #168)
chrisjwalk-bot Jun 1, 2026
55c6128
fix: use single wildcard in SWA navigationFallback exclude paths
chrisjwalk-bot Jun 1, 2026
fa24b92
chore: update GitHub Actions to Node.js 24 compatible versions
chrisjwalk-bot Jun 1, 2026
50c04fe
fix: resolve CounterContainer stub and update E2E tests for /mfe-counter
chrisjwalk-bot Jun 1, 2026
3899365
fix: build counter-remote before copying into web-app output in CI
chrisjwalk-bot Jun 1, 2026
3ad98c5
docs: update create-mfe skill with accurate patterns and pitfalls
chrisjwalk-bot Jun 1, 2026
28db1e6
fix: persist counter state across navigation by providing store at ro…
chrisjwalk-bot Jun 1, 2026
fc702cb
fix: move PWA icons to Vite publicDir so they are included in the bui…
chrisjwalk-bot Jun 7, 2026
bf95882
fix: remove deprecated provideAnimations to fix MFE loadShare bug (fi…
chrisjwalk-bot Jun 7, 2026
b158ccf
chore: update pnpm allowBuilds
chrisjwalk-bot Jun 7, 2026
6c42d85
fix: remove @ngrx/signals/events to fix MFE NG0203 error (closes #168)
chrisjwalk-bot Jun 7, 2026
0bb71d4
fix: restore @ngrx/signals requiredVersion to ~21.1.0 (matches instal…
chrisjwalk-bot Jun 7, 2026
2c7aaf7
fix: restore @ngrx/signals requiredVersion to ~21.1.0 in counter-remo…
chrisjwalk-bot Jun 7, 2026
b1973f7
fix: migrate pnpm overrides to workspace yaml, bump CI to pnpm 11
chrisjwalk-bot Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
528 changes: 528 additions & 0 deletions .claude/skills/create-mfe/SKILL.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

- uses: pnpm/action-setup@v4
with:
version: 10
version: 11

- uses: actions/setup-node@v6
with:
Expand Down
18 changes: 14 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ jobs:
environment: production

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: pnpm/action-setup@v4
with:
version: 10
version: 11

- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm

- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x

Expand All @@ -58,9 +58,19 @@ jobs:
- name: Build API (Release)
run: pnpm build:api:prod

- name: Build counter-remote (production)
run: pnpm nx build counter-remote --configuration production

- name: Build web app (production)
env:
COUNTER_REMOTE_ENTRY: /counter-remote/remoteEntry.js
run: pnpm build:web-app:prod

- name: Copy counter-remote output into web-app output
run: |
mkdir -p dist/apps/web-app/client/counter-remote
cp -r dist/apps/counter-remote/* dist/apps/web-app/client/counter-remote/

# The .NET app serves the Angular SPA from apps/web-app/client/ relative
# to its working directory (see Program.cs PhysicalFileProvider). Both
# outputs land in dist/ with the correct structure:
Expand Down
16 changes: 13 additions & 3 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,33 @@ jobs:
contents: read
pull-requests: write # SWA deploy posts preview URL comment
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: pnpm/action-setup@v4
with:
version: 10
version: 11

- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build counter-remote (production)
run: pnpm nx build counter-remote --configuration production

- name: Build web app (preview)
env:
COUNTER_REMOTE_ENTRY: /counter-remote/remoteEntry.js
run: pnpm nx build web-app --configuration preview

- name: Copy counter-remote output into web-app output
run: |
mkdir -p dist/apps/web-app/client/counter-remote
cp -r dist/apps/counter-remote/* dist/apps/web-app/client/counter-remote/

- name: Copy SWA routing config
run: cp apps/web-app/src/staticwebapp.config.json dist/apps/web-app/client/

Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
- **Notification center** — persistent notification panel with unread count, mark-as-read, dismiss, and action support (e.g. one-click reload on SW update)
- **PWA / service worker** — offline support; notifies users when a new app version is available with an in-app prompt to reload
- **Markdown content pages** — [Analog.js](https://analogjs.org) content feature renders pages from Markdown files with frontmatter support (see the [About](/about) page for a live demo)
- **Counter micro frontend** — the Counter feature runs as a separate [Module Federation](https://module-federation.io/) remote (`counter-remote`), demonstrating micro-frontend architecture with shared singleton libraries
- **Debug page** (`/debug`) — trigger test notifications and inspect service worker update state during development
- **PR preview deployments** — every pull request gets a live preview URL via Azure Static Web Apps

Expand All @@ -23,6 +24,7 @@ A full-stack demo using an [Nx monorepo](https://nx.dev) with [Angular](https://
- [Analog.js](https://analogjs.org) — Vite-native Angular meta-framework; used for file-based Markdown content pages
- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling
- [Angular PWA](https://angular.dev/ecosystem/service-workers) — service worker & offline support
- [Module Federation](https://module-federation.io/) (`@module-federation/vite`) — micro-frontend architecture

**Backend**

Expand Down Expand Up @@ -63,6 +65,33 @@ pnpm start

Starts both the .NET API and Angular app in dev mode. Open [http://localhost:4200](http://localhost:4200) for the app, or [https://localhost:60254/swagger](https://localhost:60254/swagger) for the API docs.

## Micro-frontend development

The Counter feature is a [Module Federation](https://module-federation.io/) micro-frontend remote. To develop with the MFE active, start both servers in separate terminals:

```bash
# Terminal 1 — remote (port 4201)
pnpm nx serve counter-remote

# Terminal 2 — host (port 4200)
pnpm nx serve web-app
```

The host at `http://localhost:4200` will load the Counter remote automatically from `http://localhost:4201/remoteEntry.js` when you navigate to `/mfe-counter`.

### Architecture

| App | Role | Port | Description |
| ---------------- | ---------- | ---- | --------------------------------------------- |
| `web-app` | MFE host | 4200 | Main application shell |
| `counter-remote` | MFE remote | 4201 | Counter feature exposed via Module Federation |

**Shared singletons** — Angular core, CDK/Material, and NgRx are configured as Module Federation singletons so both apps share a single instance. This prevents Angular's NG0912 component-ID collision warnings that occur when the same component class is registered twice.

**Remote self-containment** — workspace libs (`@myorg/shared`, `@myorg/counter`) are NOT in the MF shared config because Rolldown's static analysis cannot enumerate `export *` chains from TypeScript path aliases. Instead, the remote is designed to be self-contained: `counter-remote` bundles only `@myorg/counter` (which has no `@myorg/shared` dependencies), and the host bundles `@myorg/shared` exclusively (no duplicate registrations).

**Preview deployments** — in CI, `counter-remote` is built and served from `/counter-remote/` within the same Static Web App as the host. The `COUNTER_REMOTE_ENTRY` environment variable is set to `/counter-remote/remoteEntry.js` during the host build so the baked-in import map points to the co-deployed remote instead of `localhost`.

## Lint

```bash
Expand Down
13 changes: 13 additions & 0 deletions apps/counter-remote/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Counter Remote</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body class="m-0">
<app-root></app-root>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
52 changes: 52 additions & 0 deletions apps/counter-remote/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "counter-remote",
"$schema": "../../node_modules/nx/schemas/project.schema.json",
"sourceRoot": "apps/counter-remote/src",
"projectType": "application",
"tags": ["type:app"],
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/counter-remote"
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"continuous": true,
"defaultConfiguration": "development",
"options": {
"buildTarget": "counter-remote:build",
"port": 4201
},
"configurations": {
"development": {
"buildTarget": "counter-remote:build:development"
},
"production": {
"buildTarget": "counter-remote:build:production",
"hmr": false
}
}
},
"test": {
"executor": "@nx/vitest:test",
"outputs": ["{projectRoot}/../../coverage/apps/counter-remote"],
"options": {
"reportsDirectory": "{projectRoot}/../../coverage/apps/counter-remote"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
16 changes: 16 additions & 0 deletions apps/counter-remote/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
ApplicationConfig,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideHttpClient(withFetch()),
provideRouter(routes),
],
};
7 changes: 7 additions & 0 deletions apps/counter-remote/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Route } from '@angular/router';
import { counterRoutes } from '@myorg/counter';

export const routes: Route[] = [
...counterRoutes,
{ path: '**', redirectTo: '' },
];
11 changes: 11 additions & 0 deletions apps/counter-remote/src/app/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `<router-outlet />`,
host: { 'data-testid': 'app-root' },
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {}
5 changes: 5 additions & 0 deletions apps/counter-remote/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';

bootstrapApplication(App, appConfig).catch(console.error);
1 change: 1 addition & 0 deletions apps/counter-remote/src/remote-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { counterRoutes } from '@myorg/counter';
1 change: 1 addition & 0 deletions apps/counter-remote/src/test-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@myorg/shared/test-setup.shared';
11 changes: 11 additions & 0 deletions apps/counter-remote/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["vite/client", "vite-plugin-pwa/client"],
"moduleResolution": "bundler"
},
"files": ["src/main.ts", "src/remote-routes.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["vite.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}
33 changes: 33 additions & 0 deletions apps/counter-remote/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "es2022",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve",
"moduleResolution": "bundler",
"lib": ["dom", "es2022"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true,
"typeCheckHostBindings": true
}
}
22 changes: 22 additions & 0 deletions apps/counter-remote/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"vite-plugin-pwa/client",
"node",
"vitest"
],
"isolatedModules": true
},
"include": [
"vite.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
],
"files": ["src/test-setup.ts"]
}
Loading
Loading