AgentSkillsCN

microfrontend-nx

为 Breeze Airways 运营前端工作空间打造 NX 单体微前端模式。严格遵循模块联邦架构、功能范围内的库结构、共享平台规范以及路由规则。适用于在运营前端编写或修改 React/TypeScript 代码时使用。

SKILL.md
--- frontmatter
name: microfrontend-nx
description: NX monorepo microfrontend patterns for the Breeze Airways ops-fe workspace. Enforces Module Federation architecture, feature-scoped library structure, shared platform conventions, and routing rules. Use when writing or modifying React/TypeScript code in the operations frontend.

Microfrontend NX Skill — Operations Frontend

Patterns and conventions for the Breeze Airways ops-fe NX monorepo. This workspace implements a Module Federation microfrontend architecture with a single host app (ns2) orchestrating feature domains via React 19, Rspack, and TypeScript.

Architecture Overview

A host application loads feature domains as local routes and remote MFE applications at runtime via Module Federation.

code
Browser (SPA)
├── ns2 (HOST) :4200 — BrowserRouter
│   ├── /access-mgmt, /disruption, /crew-notifications, ... (local features)
│   └── /flight-story/* → React.lazy(() => import('flight_story/Module'))
│                           ↓ Module Federation runtime loading
├── flight_story (REMOTE) :4201 — HashRouter, exposes ./Module
└── turn (STANDALONE) :4200 — No Module Federation

Key rules:

  • Host uses BrowserRouter, remotes use HashRouter (prevents route collisions)
  • Host owns the URL pathname (/flight-story), remote owns the hash fragment (/#/details/123)
  • Shared singletons (React, Apollo, MSAL) resolve from the host — not duplicated
  • All apps are client-rendered SPAs (no SSR), all React 19

Directory Structure

code
ops-fe/
├── apps/
│   ├── ns2/                          # HOST application
│   │   ├── module-federation.config.ts
│   │   ├── rspack.config.ts
│   │   ├── rspack.config.prod.ts
│   │   ├── project.json
│   │   └── src/
│   │       ├── bootstrap.tsx         # MSAL init + app mount
│   │       ├── main.ts              # Entry point (import bootstrap)
│   │       └── app/
│   │           └── app.tsx           # Routes + lazy remote import
│   │
│   ├── flight_story/                 # REMOTE application
│   │   ├── module-federation.config.ts
│   │   ├── rspack.config.ts
│   │   ├── rspack.config.prod.ts
│   │   ├── project.json
│   │   └── src/
│   │       ├── bootstrap.tsx
│   │       ├── main.ts
│   │       ├── remote-entry.ts       # MFE entry (re-exports app)
│   │       └── app/
│   │           └── app.tsx           # HashRouter + routes
│   │
│   └── turn/                         # STANDALONE application
│
├── libs/
│   ├── shared/                       # Platform-level shared code
│   │   ├── shared-ui/                # Reusable UI components
│   │   ├── shared-hooks/             # React hooks
│   │   ├── shared-models/            # Types and interfaces
│   │   ├── shared-services/          # Business logic helpers
│   │   ├── shared-utils/             # Utility functions, clientFactory
│   │   ├── shared-configs/           # MSAL, Sentry, NewRelic, MFE config
│   │   ├── shared-contexts/          # React Contexts
│   │   ├── shared-graphql/           # GraphQL queries/mutations
│   │   └── shared-assets/            # Icons, SVGs (via SVGR)
│   │
│   ├── {feature}/                    # Feature-scoped libraries
│   │   ├── {feature}-components/     # UI components for this domain
│   │   └── {feature}-hooks/          # Data hooks for this domain
│   │
│   └── ns2/                          # Host app-specific libs
│       ├── ns2-components/
│       └── ns2-hooks/
│
├── environments/
│   ├── environment.ts                # Dev/staging config
│   └── environment.cicd.ts           # CI/CD config
│
├── nx.json
├── tsconfig.base.json
├── package.json
└── tailwind.config.js

Feature Domains

18 feature domains, each with its own libs/{feature}/ directory:

DomainLibrary Path
access-managementlibs/access-management/
bag-bunglerlibs/bag-bungler/
bag-tag-scannerlibs/bag-tag-scanner/
crew-notificationslibs/crew-notifications/
disruption-loglibs/disruption-log/
employee-travellibs/employee-travel/
far-monitorlibs/far-monitor/
flight-linelibs/flight-line/
flight-storylibs/flight-story/
fuel-sliplibs/fuel-slip/
guest-notificationslibs/guest-notifications/
load-sheetlibs/load-sheet/
notificationslibs/notifications/
ns2libs/ns2/
ssimlibs/ssim/
ssr-portallibs/ssr-portal/
turnlibs/turn/

Core Patterns

1. TypeScript Path Aliases

All imports use @ops-fe/ scoped aliases defined in tsconfig.base.json:

typescript
// Shared platform libraries
import { SomeComponent } from '@ops-fe/shared-ui';
import { useAuth } from '@ops-fe/shared-hooks';
import { FlightModel } from '@ops-fe/shared-models';
import { clientFactory } from '@ops-fe/shared-utils';
import { msalConfig } from '@ops-fe/shared-configs';
import { AppContext } from '@ops-fe/shared-contexts';
import { GET_FLIGHTS } from '@ops-fe/shared-graphql';

// Feature-scoped libraries
import { FlightCard } from '@ops-fe/{feature}-components';
import { useFlightData } from '@ops-fe/{feature}-hooks';

// Assets (wildcard path)
import SomeIcon from '@ops-fe/shared-assets/icons/SomeIcon.svg';

Rules:

  • Never use relative paths across library boundaries — always use @ops-fe/ aliases
  • Each library exports through a single src/index.ts barrel file
  • Feature libs only import from shared/ libs, never from other feature libs

2. Module Federation Configuration

Host (apps/ns2/module-federation.config.ts):

typescript
import { ModuleFederationConfig } from '@nx/module-federation';
import { MFE_PACKAGE_CONFIG } from '@ops-fe/shared-configs';

const config: ModuleFederationConfig = {
  name: 'ns2',
  remotes: ['flight_story'],
  shared: (name, config) => {
    if (MFE_PACKAGE_CONFIG.includes(name)) return false;
    return config;
  },
};
export default config;

Remote (apps/flight_story/module-federation.config.ts):

typescript
const config: ModuleFederationConfig = {
  name: 'flight_story',
  exposes: {
    './Module': './src/remote-entry.ts',
  },
  shared: (name, config) => {
    if (MFE_PACKAGE_CONFIG.includes(name)) return false;
    return config;
  },
};
export default config;

Singleton exclusions (libs/shared/shared-configs/src/lib/mfe-package-config.ts):

typescript
export const MFE_PACKAGE_CONFIG: string[] = [
  '@apollo/client/link/retry',
  '@apollo/client/link/context',
  '@radix-ui/react-slot',
  'react-phone-number-input/input',
  'react-phone-number-input/locale/en',
];

These packages are excluded from Module Federation's singleton sharing to avoid version conflicts.

3. Application Bootstrap Pattern

Every app follows: main.ts → async bootstrap.tsx<App />

typescript
// main.ts — entry point (async boundary for Module Federation)
import('./bootstrap');

// bootstrap.tsx — initialize auth, mount app
import { PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';

const msalInstance = new PublicClientApplication(msalConfig);
// ... MSAL init
root.render(
  <MsalProvider instance={msalInstance}>
    <App />
  </MsalProvider>
);

The async boundary in main.ts is required — it allows Module Federation to negotiate shared dependencies before app code executes.

4. Remote Loading Pattern

Remotes are loaded in the host via React.lazy + Suspense:

typescript
// apps/ns2/src/app/app.tsx
const FlightStory = React.lazy(() => import('flight_story/Module'));

// In routes:
<React.Suspense fallback={<LoadingSpinner />}>
  <FlightStory />
</React.Suspense>

Always wrap remote components with an error boundary to prevent host crash on remote load failure.

5. Router Isolation

code
HOST (ns2) — BrowserRouter          REMOTE (flight_story) — HashRouter
  /flight-story/*  ────────────▶      /#/
  /access-management/*                /#/:id
  /disruption-log/*                   /#/simulator/flights
  /crew-notifications/*               /#/simulator/load-sheet
  ...

Rules:

  • Host always uses BrowserRouter
  • Remote MFEs always use HashRouter
  • Never mix router types within a single app

6. Feature-Scoped Library Pattern

Each feature domain isolates its code in libs/{feature}/:

code
libs/disruption-log/
├── disruption-log-components/
│   └── src/
│       ├── index.ts              # Barrel export
│       └── lib/
│           ├── DisruptionForm.tsx
│           └── DisruptionTable.tsx
└── disruption-log-hooks/
    └── src/
        ├── index.ts              # Barrel export
        └── lib/
            ├── useDisruptionLog.ts
            └── useDisruptionFilters.ts

Rules:

  • -components libs contain React UI components
  • -hooks libs contain data-fetching hooks (Apollo queries, state logic)
  • Feature libs may import from shared/ but never from other feature libs
  • Cross-feature logic belongs in a shared/ library

Shared Dependency Resolution

These packages are shared as singletons across host and remotes via Module Federation:

PackageRole
react / react-domUI framework (single instance required)
react-routerRouting
@apollo/clientGraphQL client
@azure/msal-reactAuthentication

The MFE_PACKAGE_CONFIG list excludes specific sub-packages from sharing (loaded independently per MFE).

Authentication Flow

  1. bootstrap.tsx initializes MSAL PublicClientApplication
  2. Azure AD login returns JWT with name and employee_number
  3. Token feeds into MsalProvider (auth state), Apollo (Bearer header), and GrowthBook (user attributes)
  4. Host and remotes share the MSAL instance through Module Federation singletons
  5. Unauthenticated users see UnauthenticatedTemplate (login page)

Rspack Configuration

Each app has two Rspack configs:

Development (rspack.config.ts):

typescript
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin } = require('@nx/module-federation/rspack');
const { NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');

module.exports = {
  output: { uniqueName: 'ns2', publicPath: 'auto' },
  devServer: { port: 4200, historyApiFallback: true },
  plugins: [
    new NxAppRspackPlugin({ /* assets, styles, scripts */ }),
    new NxReactRspackPlugin({ svgr: true }),
    new NxModuleFederationPlugin({ config: mfConfig }),
    new NxModuleFederationDevServerPlugin({ config: mfConfig }),
  ],
};

Production (rspack.config.prod.ts) extends dev config with xxhash64 hashing.

Rules:

  • Always use Rspack (not Webpack) — @nx/rspack plugins
  • SVGR is enabled (svgr: true) for importing SVGs as React components
  • publicPath: 'auto' is required for Module Federation
  • historyApiFallback: true is required for SPA routing

Environment Configuration

typescript
// environments/environment.ts
export const environment = {
  production: false,
  azureAd: { clientId: '...', apiScope: '...' },
  urls: { ns2: '...', disruptionLog: '...', operationsBackend: '...' },
  growthBook: { clientKey: '...' },
  firebase: { projectId: '...', vapidKey: '...' },
  scandit: { licenseKey: '...' },
  newRelic: { licenseKey: '...', accountId: '...' },
};

Rules:

  • No secrets in client bundles
  • Use environment.ts for dev/staging, environment.cicd.ts for CI/CD
  • All API URLs come from environment config, never hardcoded

NX Commands Reference

bash
# Serve host with all remotes
npx nx serve ns2

# Serve individual remote
npx nx serve flight_story

# Serve host with specific remote in dev mode
NX_MF_DEV_REMOTES=flight_story npx nx serve ns2

# Build for production
npx nx run-many -t build --configuration=cicd --verbose

# Run all tests
npx nx run-many -t test

# Run tests for a specific library
npx nx test shared-ui

# Run affected tests only
npx nx affected -t test

# Generate a new library
npx nx g @nx/react:lib my-feature-components --directory=libs/my-feature

# Generate a new remote MFE app
npx nx g @nx/react:remote my-remote --host=ns2

# Visualize dependency graph
npx nx graph

Testing

  • Unit Tests: Jest + @testing-library/react. Each library has its own test config via NX project inference.
  • Component Dev: Storybook (uses Vite builder, separate from Rspack).
  • Linting: ESLint (flat config) + Prettier.
  • CI: npx nx run-many -t test runs all affected tests. NX caching skips unchanged projects.

Observability

ToolPurpose
Sentry (@sentry/react)Error tracking and source maps
New Relic (@newrelic/browser-agent)Performance monitoring and RUM
FirebasePush notification delivery tracking

Error Handling

ScenarioRecovery
Remote fails to loadSuspense fallback UI; user can retry navigation
Auth token expiredMSAL auto-refreshes; redirect to login on failure
GraphQL errorApollo error policies per query; toast notifications
Feature flag unavailableGrowthBook defaults to false (feature hidden)

Technology Stack

CategoryTechnology
FrameworkReact 19
LanguageTypeScript
BundlerRspack (via @nx/rspack)
MFE RuntimeModule Federation (Enhanced)
MonorepoNX
RoutingReact Router 7
GraphQLApollo Client
AuthAzure MSAL
Feature FlagsGrowthBook
CSSTailwind CSS
UI ComponentsMUI
TestingJest + Testing Library
Component DevStorybook
CI/CDGitLab CI

Adding a New Feature Checklist

When adding a new feature domain:

  1. Generate librariesnpx nx g @nx/react:lib {feature}-components --directory=libs/{feature} and {feature}-hooks
  2. Add path aliases — Update tsconfig.base.json with @ops-fe/{feature}-components and @ops-fe/{feature}-hooks
  3. Create components — Add UI components in {feature}-components/src/lib/
  4. Create hooks — Add data-fetching hooks in {feature}-hooks/src/lib/
  5. Export via barrel — Update each library's src/index.ts
  6. Add route — Add route in apps/ns2/src/app/app.tsx (or remote app if applicable)
  7. Add shared types — If types are cross-feature, add to shared-models
  8. Add GraphQL operations — If new queries/mutations, add to shared-graphql or feature hooks
  9. Test — Unit tests per library, npx nx test {feature}-components

Promoting a Feature to Remote MFE

When a feature domain needs independent deployment:

  1. Generate remote appnpx nx g @nx/react:remote {feature} --host=ns2
  2. Add Module Federation config — Create module-federation.config.ts with exposes: { './Module': './src/remote-entry.ts' }
  3. Use HashRouter — Remote app must use HashRouter, not BrowserRouter
  4. Create remote-entry.ts — Re-export the app component
  5. Update host config — Add remote name to remotes array in host's module-federation.config.ts
  6. Add lazy import in hostReact.lazy(() => import('{feature}/Module')) wrapped in Suspense + error boundary
  7. Add type declaration — Add "{feature}/Module" path alias in tsconfig.base.json