{"id":50762418,"url":"https://github.com/devlinduldulao/module-federation-demo","last_synced_at":"2026-06-11T11:02:08.683Z","repository":{"id":315532455,"uuid":"1054161653","full_name":"devlinduldulao/module-federation-demo","owner":"devlinduldulao","description":"A demonstration of Module Federation with independently deployed micro-frontends. Each module runs on its own port, ships its own bundle, and can be developed in isolation.","archived":false,"fork":false,"pushed_at":"2026-04-22T15:27:09.000Z","size":1321,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-01T19:07:28.965Z","etag":null,"topics":["microfrontends","module-federation","suspense"],"latest_commit_sha":null,"homepage":"https://devlinduldulao.github.io/module-federation-demo/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devlinduldulao.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-10T13:00:07.000Z","updated_at":"2026-04-22T15:28:12.000Z","dependencies_parsed_at":"2025-09-19T21:47:29.922Z","dependency_job_id":null,"html_url":"https://github.com/devlinduldulao/module-federation-demo","commit_stats":null,"previous_names":["webmasterdevlin/module-federation-demo","devlinduldulao/module-federation-demo"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/devlinduldulao/module-federation-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlinduldulao%2Fmodule-federation-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlinduldulao%2Fmodule-federation-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlinduldulao%2Fmodule-federation-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlinduldulao%2Fmodule-federation-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devlinduldulao","download_url":"https://codeload.github.com/devlinduldulao/module-federation-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlinduldulao%2Fmodule-federation-demo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34195117,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["microfrontends","module-federation","suspense"],"created_at":"2026-06-11T11:02:07.458Z","updated_at":"2026-06-11T11:02:08.676Z","avatar_url":"https://github.com/devlinduldulao.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Module Federation Demo\n\nA micro-frontend architecture demo built with **Rspack Module Federation**, **React 19**, **TypeScript**, and **Tailwind CSS v4**. Five independent applications compose into a single shell — each deployable, scalable, and maintainable on its own.\n\nBuilt for conference talks and technical demonstrations.\n\n## Architecture\n\n```\nShell (host)           localhost:3000\n├── Home (remote)      localhost:3004   → INSTANT  (Home — no streaming delay)\n├── Records (remote)  localhost:3001   → EAGER    (MedicalRecords — preloaded on shell mount)\n├── Prescriptions (remote)      localhost:3002   → STREAMED (StreamingPrescriptionOrders — on demand)\n└── Analytics (remote) localhost:3003   → STREAMED (StreamingClinicalAnalytics — on demand)\n```\n\nEach remote exposes both a **Streaming** component (wraps a Resource-based Suspense pattern to simulate network delay) and a **Standalone** component (renders immediately). The shell chooses which to import based on **three loading strategies** and content priority:\n\n| Strategy | Module | Behavior |\n|----------|--------|----------|\n| **Instant** | Home | Lazy-loaded for code splitting, but imports the standalone component directly — no streaming delay. Renders the moment the chunk arrives. |\n| **Eager** | Records | Imports the standalone component directly and preloads the chunk on shell mount — already cached before the user clicks. No skeleton, no streaming delay. Still uses `lazy()` because Module Federation remotes are separate builds resolved at runtime via `import()` — you can't use a static `import`. The eager `import()` fires at shell init and warms the cache; `lazy()` resolves from it instantly. |\n| **Streamed** | Prescriptions, Analytics | Loaded on demand with per-module skeleton fallbacks and `\u003cErrorBoundary\u003e` for fault isolation. |\n\nAll modules are wrapped in `\u003cSuspense\u003e` with per-module skeleton fallbacks and `\u003cErrorBoundary\u003e` for fault isolation. The shell owns URL-based navigation, so `/`, `/records`, `/prescriptions`, and `/analytics` are directly shareable routes. The status strip shows the active module's loading strategy (INSTANT / EAGER / STREAMING) with a color-coded indicator.\n\nThe root route `/` renders the **Home** landing page, which provides an overview of the architecture and navigation cards to each module. Unknown routes redirect to `/`.\n\n## Quick Start\n\nPrerequisites: Node.js 20+ and `pnpm`. If `pnpm` is not installed yet, run `corepack enable` first.\n\n```bash\n# Install everything (root + all 5 packages)\npnpm install\n\n# Start all five dev servers concurrently\npnpm run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000). The shell renders the Home landing page at `/` and pulls remote entry points from ports 3001–3004.\n\nIf `pnpm run dev` fails, the most common cause is that one of the demo ports is already occupied. Use `pnpm run kill:ports` to clear the demo ports and retry.\n\n### Quality Checks\n\n```bash\n# Lint every package\npnpm run lint\n\n# Run TypeScript checks across the workspace\npnpm run typecheck\n\n# Run all tests\npnpm test\n\n# Build every package\npnpm run build\n```\n\n### Run a single package (standalone development)\n\nEach module is a fully self-contained React app. You can open any package folder in its own VS Code window and develop with full HMR — no other modules need to be running:\n\n```bash\ncd packages/home \u0026\u0026 pnpm install \u0026\u0026 pnpm run dev          # :3004\ncd packages/records \u0026\u0026 pnpm install \u0026\u0026 pnpm run dev       # :3001\ncd packages/prescriptions \u0026\u0026 pnpm install \u0026\u0026 pnpm run dev # :3002\ncd packages/analytics \u0026\u0026 pnpm install \u0026\u0026 pnpm run dev     # :3003\ncd packages/shell \u0026\u0026 pnpm install \u0026\u0026 pnpm run dev         # :3000\n```\n\nEach remote runs standalone at its own port with its own `index.html`. The shell also runs standalone — remotes it can't reach will show `ModuleFallback` instead of crashing.\n\n**What works standalone:** `dev` (with HMR), `build`, `typecheck`\n\n**What needs the monorepo root:** `lint` (shared ESLint config), `test` / `test:watch` (shared Vitest config)\n\n#### The async bootstrap pattern (required for standalone mode)\n\nEvery package's `index.tsx` uses a dynamic import:\n\n```ts\n// index.tsx — thin async entry point\nimport(\"./bootstrap\");\n```\n\n```tsx\n// bootstrap.tsx — actual React rendering\nimport React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport MedicalRecords from \"./MedicalRecords\";\n\nconst root = ReactDOM.createRoot(document.getElementById(\"root\")!);\nroot.render(\u003cMedicalRecords /\u003e);\n```\n\nThis is **required** because Module Federation declares `react` and `react-dom` as `shared` with `eager: false`. The dynamic `import()` creates an async boundary that lets Module Federation negotiate shared dependencies before any React code runs. Without it, standalone mode fails with `loadShareSync` errors and you get a white screen.\n\n\u003e **Do not remove the bootstrap pattern.** If you merge `bootstrap.tsx` back into `index.tsx`, standalone dev will break with `Invalid loadShareSync function call` errors. The shell has always had this pattern; all remotes now have it too.\n\n#### CSS ownership in federated remotes\n\nFor remotes, the **exposed component must import its own stylesheet**:\n\n```tsx\n// MedicalRecords.tsx — exposed by Module Federation\nimport \"./index.css\";\n```\n\nDo **not** rely on `bootstrap.tsx` to load remote CSS. `bootstrap.tsx` only runs in standalone mode, but the shell imports the exposed component directly from `remoteEntry.js`. If the CSS import lives only in bootstrap, the remote looks correct at `localhost:3001` and then loses spacing or utility styles when mounted inside the shell.\n\n### Prefetching + Eager Loading\n\nThe shell uses a **two-tier preloading strategy**:\n\n1. **Eager preload** — modules with `loadStrategy: \"eager\"` (Records) are preloaded the moment the shell mounts, so their chunks and streaming data are likely cached before the user navigates.\n2. **Hover prefetch** — remaining modules are prefetched when the user hovers a navigation tab, using a `PREFETCHERS` map of bare `import()` calls.\n\n## Project Structure\n\n```\nmodule-federation-demo/\n├── eslint.config.mjs                  # Shared ESLint flat config\n├── package.json                       # Workspace scripts (concurrently)\n└── packages/\n    ├── shell/                         # Host application\n    │   ├── rspack.config.ts           # MF remotes config\n    │   ├── src/\n    │   │   ├── App.tsx                # Navigation, Suspense orchestration\n    │   │   ├── App.test.tsx           # Shell integration tests\n    │   │   ├── bootstrap.tsx          # createRoot entry\n    │   │   ├── index.css              # Design system tokens + animations\n    │   │   ├── types.d.ts             # Remote module declarations\n    │   │   └── components/\n    │   │       ├── ErrorBoundary.tsx   # Per-module error isolation\n    │   │       ├── ModuleFallback.tsx  # Offline module placeholder\n    │   │       ├── DemoPanel.tsx       # Federation Lab demo controls\n    │   │       ├── LoadingSpinner.tsx  # Generic loading dots\n    │   │       ├── HomeSkeleton.tsx\n    │   │       ├── RecordsSkeleton.tsx\n    │   │       ├── PrescriptionsSkeleton.tsx\n    │   │       └── AnalyticsSkeleton.tsx\n    │   └── lib/\n    │       ├── theme.ts               # Theme registry, persistence, window bridge\n    │       ├── health.ts              # Remote health monitoring (useRemoteHealth)\n    │       ├── demo.ts                # Kill switch + version registry hooks\n    │       └── utils.ts               # cn() — clsx + tailwind-merge\n    ├── home/                          # Remote — landing page\n    │   ├── rspack.config.ts           # MF exposes config\n    │   └── src/\n    │       ├── Home.tsx               # Landing page with architecture overview\n    │       ├── StreamingHome.tsx      # Suspense-wrapped\n    │       ├── types.ts\n    │       └── lib/utils.ts           # cn() utility\n    ├── records/                      # Remote — medical records\n    │   ├── rspack.config.ts           # MF exposes config\n    │   └── src/\n    │       ├── MedicalRecords.tsx     # Standalone version\n    │       ├── MedicalRecords.test.tsx\n    │       ├── StreamingMedicalRecords.tsx  # Suspense-wrapped\n    │       ├── types.ts\n    │       └── lib/utils.ts           # cn() utility\n    ├── prescriptions/                          # Remote — prescription orders\n    │   ├── rspack.config.ts\n    │   └── src/\n    │       ├── PrescriptionOrders.tsx\n    │       ├── PrescriptionOrders.test.tsx\n    │       ├── StreamingPrescriptionOrders.tsx\n    │       ├── types.ts\n    │       └── lib/utils.ts           # cn() utility\n    └── analytics/                     # Remote — clinical analytics\n        ├── rspack.config.ts\n        └── src/\n            ├── ClinicalAnalytics.tsx\n            ├── ClinicalAnalytics.test.tsx\n            ├── StreamingClinicalAnalytics.tsx\n            ├── types.ts\n            └── lib/utils.ts           # cn() utility\n```\n\n## Tech Stack\n\n| Tool | Version | Role |\n|------|---------|------|\n| React | ^19.2.5 | UI library |\n| TypeScript | ^6.0.3 | Type safety |\n| Rspack | ^2.0.0 | Bundler + Module Federation |\n| Tailwind CSS | v4 | Utility-first CSS via `@theme` |\n| PostCSS | ^8.5.10 | CSS pipeline (`@tailwindcss/postcss`) |\n| Vitest | ^4.1.5 | Unit + component testing |\n| concurrently | ^9.2.1 | Dev server orchestration |\n\n## Design System — \"Noir Editorial\"\n\nA typographic editorial design language that avoids generic pastel AI aesthetics. The default presentation is dark, and the shell can switch between `dark` and `light` palettes by updating shared CSS custom properties at runtime.\n\n### Typography\n\n| Role | Font | Usage |\n|------|------|-------|\n| Display | Instrument Serif | Headlines, large numbers (italic) |\n| Body | DM Sans | Paragraphs, UI text |\n| Technical | IBM Plex Mono | Labels, prices, metadata, navigation |\n\n### Color Tokens\n\nThe values below are the default dark theme tokens. The shell persists the active theme in `localStorage` under `mf-demo-theme` and rewrites these CSS variables when the user changes themes.\n\n| Token | Hex | Usage |\n|-------|-----|-------|\n| `noir` | `#0C0C0C` | Canvas / page background |\n| `surface` | `#141414` | Hover / elevated cards |\n| `elevated` | `#1C1C1C` | Skeleton placeholders |\n| `edge` | `#2E2E2E` | Borders, 1px grid dividers |\n| `cream` | `#FAFAF9` | Primary text |\n| `stone` | `#A8A29E` | Secondary text |\n| `dim` | `#6B6560` | Tertiary / disabled text |\n| `citrine` | `#D4FF00` | Primary accent — CTAs, active nav |\n| `mint` | `#34D399` | Success states |\n| `ice` | `#60A5FA` | Info / cool data |\n| `burnt` | `#FF6B35` | Warnings / warm accent |\n| `rose` | `#F87171` | Errors / destructive |\n\n### Key Visual Patterns\n\n- **1px grid gaps** — `gap-[1px] bg-edge` creates sharp editorial grid lines\n- **Mono uppercase labels** — `font-mono text-[11px] tracking-[0.3em] uppercase`\n- **Serif italic headings** — `font-display italic` for display type\n- **Citrine underline navigation** — active tab gets a 2px citrine bottom bar\n- **Noise grain overlay** — subtle SVG noise on `body::after`\n- **Staggered entry animations** — `fadeInUp` with incremental `animationDelay`\n\n## Module Federation Setup\n\nEach `rspack.config.ts` is a standard Rspack configuration — `entry`, `module.rules`, `devServer`, `optimization`, `resolve` are all normal bundler settings that any app would have. **The only property that transforms separate apps into a federated architecture** is the `ModuleFederationPlugin` inside `plugins`, and specifically these three sub-properties:\n\n| Property | Defined on | What it does |\n|----------|-----------|-------------|\n| **`exposes`** | Remotes | Declares the module's public API — which components other apps can import. This is the **contract** between teams. |\n| **`remotes`** | Host (shell) | Tells the host where to find each remote's `remoteEntry.js` at runtime. Format: `scope@URL`. This is the **runtime discovery mechanism**. |\n| **`shared`** | Both | Declares which dependencies should be deduplicated across the federation. `singleton: true` on React ensures one instance — without it, each remote loads its own React and hooks break. |\n\n```\nRemote (records :3001)              Shell (host :3000)\n┌──────────────────────┐            ┌──────────────────────┐\n│ exposes:             │            │ remotes:             │\n│   ./MedicalRecords ──┼──remoteEntry.js──┼─► records@:3001│\n│                      │            │                      │\n│ shared:              │            │ shared:              │\n│   react: singleton ──┼────────────┼─► react: singleton   │\n│                      │            │   (one copy for all) │\n└──────────────────────┘            └──────────────────────┘\n```\n\nEverything else in the config is standard Rspack. Remove the `ModuleFederationPlugin`, and you have five normal, unrelated apps. Add it back, and they become a federated architecture.\n\nOne practical rule follows from that: if a file is listed under `exposes`, treat it as a real runtime entrypoint. It must bring along any required CSS or other side-effect imports on its own instead of depending on standalone-only bootstrap code.\n\n## Rspack 2 Notes\n\nThis repository now runs on **Rspack 2**. A few setup details matter for anyone copying this setup:\n\n- Local development scripts use `rspack dev`, backed by an explicit `@rspack/dev-server` dependency in each package that runs a dev server.\n- `ModuleFederationPlugin` now requires an explicit `@module-federation/runtime-tools` dependency in each federated package.\n- The configs use `rspack.config.ts` + `defineConfig` and Rspack 2's cleaner defaults: `target: [\"web\", \"es2020\"]` and `detectSyntax: \"auto\"` on `builtin:swc-loader`.\n- CSS now uses Rspack's built-in CSS handling with `type: \"css\"` and keeps `postcss-loader` only for Tailwind/PostCSS transforms.\n\n### Shell (Host)\n\n```ts\n// rspack.config.ts — shell\nnew rspack.container.ModuleFederationPlugin({\n  name: \"shell\",\n  remotes: {\n    home:     \"home@http://localhost:3004/remoteEntry.js\",\n    records: \"records@http://localhost:3001/remoteEntry.js\",\n    prescriptions:     \"prescriptions@http://localhost:3002/remoteEntry.js\",\n    analytics:\"analytics@http://localhost:3003/remoteEntry.js\",\n  },\n  shared: {\n    react:              { singleton: true, strictVersion: false },\n    \"react-dom\":        { singleton: true, strictVersion: false },\n    \"react-dom/client\": { singleton: true, strictVersion: false },\n  },\n});\n```\n\n### Remote (example: records)\n\n```ts\n// rspack.config.ts — records\nnew rspack.container.ModuleFederationPlugin({\n  name: \"records\",\n  filename: \"remoteEntry.js\",\n  exposes: {\n    \"./MedicalRecords\":          \"./src/MedicalRecords.tsx\",\n    \"./StreamingMedicalRecords\": \"./src/StreamingMedicalRecords.tsx\",\n  },\n  shared: { react: { singleton: true }, \"react-dom\": { singleton: true } },\n});\n```\n\n## Inter-Module Communication\n\nModules communicate through typed `CustomEvent` dispatch on `window`:\n\n```typescript\n// Records → Prescriptions: add item\nwindow.dispatchEvent(\n  new CustomEvent(\"addPrescription\", {\n    detail: { id: 1, name: \"Sarah Chen\", price: 999.99, quantity: 1 },\n    bubbles: true,\n  })\n);\n\n// Any module → Shell: trigger notification toast\nwindow.dispatchEvent(\n  new CustomEvent(\"showNotification\", {\n    detail: { type: \"success\", message: \"Prescription created\" },\n  })\n);\n\n// Shell: notify on tab change\nwindow.dispatchEvent(\n  new CustomEvent(\"moduleChange\", {\n    detail: { newModule: \"prescriptions\" },\n  })\n);\n\n// Remote -\u003e Shell: request host-owned navigation without importing the router\nwindow.dispatchEvent(\n  new CustomEvent(\"navigateToModule\", {\n    detail: { module: \"records\" },\n  })\n);\n\n// Shell: broadcast a theme change to remotes\nwindow.dispatchEvent(\n  new CustomEvent(\"themeChange\", {\n    detail: { theme: \"light\", colorScheme: \"light\" },\n  })\n);\n```\n\nEvents are typed in each package's `types.ts` via `WindowEventMap` augmentation:\n\n```typescript\nexport interface AddPrescriptionEvent extends CustomEvent {\n  detail: PrescriptionItem;\n}\n\ndeclare global {\n  interface WindowEventMap {\n    addPrescription: AddPrescriptionEvent;\n    navigateToModule: CustomEvent\u003c{ module: \"home\" | \"records\" | \"prescriptions\" | \"analytics\" }\u003e;\n    showNotification: NotificationEvent;\n    themeChange: ThemeChangeEvent;\n  }\n}\n```\n\nThe shell also exposes `window.__MF_THEME__` so remotes can read or update the active theme without importing host-only shell code.\n\n## Shell Controls\n\nThe shell header exposes three control surfaces — **Settings**, **Commands**, and **Lab** — that serve distinct purposes during both development and live presentations. Each opens as a slide-over panel or overlay.\n\n### Settings (Appearance Drawer)\n\n**Purpose:** Shell-owned theme control with live persistence and cross-module broadcasting.\n\nClick the **Settings** button in the header to open a slide-over drawer from the right. It provides:\n\n- **Theme selection** — toggle between Dark and Light palettes. Each option shows a description and an \"Active\" / \"Available\" badge.\n- **Live propagation** — selecting a theme immediately rewrites CSS custom properties on `:root`, persists the choice in `localStorage` under `mf-demo-theme`, and dispatches a typed `themeChange` event on `window`. All remotes react without a page refresh.\n- **Persistence indicator** — the drawer footer confirms the `localStorage` key being used.\n\n**Why it matters for the demo:** This proves that the shell can own shared UI state (theme) and broadcast changes across independently deployed remotes through CSS variables and events — no shared imports, no prop drilling across module boundaries.\n\nThe header also includes an inline **Dark / Light** toggle for quick switching without opening the full drawer.\n\n**Implementation (`lib/theme.ts` + `App.tsx` + `SettingsDrawer`):**\n\nThe theme system is built on three layers:\n\n1. **Theme registry** — `THEME_DEFINITIONS` maps each theme name to a label, description, color scheme, and a complete set of CSS custom property values. Adding a new theme means adding one entry here.\n\n2. **`applyTheme()` function** — the single entry point for all theme changes. It:\n   - Sets `data-theme` and `color-scheme` on `\u003chtml\u003e` so CSS can target the active theme\n   - Iterates `definition.variables` and calls `root.style.setProperty()` for each token — this is how every remote's Tailwind classes update instantly\n   - Exposes `window.__MF_THEME__` (a getter/setter bridge) so remotes can read or change the theme without importing shell code\n   - Persists to `localStorage` (wrapped in try/catch for private browsing)\n   - Dispatches a typed `themeChange` CustomEvent so remotes listening via `useActiveTheme()` can react\n\n3. **Shell state** — `App.tsx` holds `theme` in `useState`, calls `applyTheme(theme)` in a `useEffect`, and listens for `StorageEvent` to sync theme changes across browser tabs. The `SettingsDrawer` component is a `memo`-wrapped overlay that receives `theme` and `onSelectTheme` as props — pure presentation, no side effects.\n\n```typescript\n// lib/theme.ts — core propagation\nexport function applyTheme(theme: ThemeName): void {\n  const definition = THEME_DEFINITIONS[theme];\n  const root = document.documentElement;\n\n  root.dataset.theme = theme;\n  root.style.colorScheme = definition.colorScheme;\n\n  for (const [variable, value] of Object.entries(definition.variables)) {\n    root.style.setProperty(variable, value);  // ← every remote's CSS reacts\n  }\n\n  window.__MF_THEME__ = {                    // ← remote bridge\n    getTheme: () =\u003e theme,\n    setTheme: (next) =\u003e applyTheme(next),\n  };\n\n  localStorage.setItem(THEME_STORAGE_KEY, theme);\n\n  window.dispatchEvent(                      // ← event contract\n    new CustomEvent(\"themeChange\", {\n      detail: { theme, colorScheme: definition.colorScheme },\n    })\n  );\n}\n```\n\n### Commands (Command Palette)\n\n**Purpose:** A keyboard-first command palette (VS Code–style) for navigating, theming, and controlling the demo without touching the mouse.\n\nPress **Ctrl+K** (or **Cmd+K** on Mac), or click the **Commands** button to open a search overlay. It provides:\n\n- **Navigation commands** — \"Switch to Records\", \"Switch to Prescriptions\", etc. Each shows the module name and port number.\n- **Theme commands** — \"Apply Dark Theme\", \"Apply Light Theme\" with descriptions.\n- **Demo commands** — \"Open Federation Lab\", \"Kill/Restore \\\u003cmodule\\\u003e Remote\", \"Switch to Canary/Stable Ring\".\n- **Fuzzy search** — type any keyword (module name, port, \"kill\", \"canary\", \"dark\") and the list filters in real time.\n- **Keyboard dismiss** — press Esc to close.\n\n**Why it matters for the demo:** During a live talk, the speaker can control the entire demo from the keyboard — navigate between modules, kill remotes, toggle themes, and switch deployment rings — without hunting for buttons. It also demonstrates that shell-level orchestration features (kill switch, version registry) are accessible from multiple surfaces: the Lab panel, the command palette, and the status strip.\n\n**Implementation (`App.tsx` — `CommandPalette` + `commandActions`):**\n\nThe command palette is a `memo`-wrapped overlay component that receives a `commands` array and a `query` string as props. The actual command definitions are built in `ShellFrame` via `useMemo`:\n\n1. **Command generation** — `commandActions` is a memoized array built from three sources:\n   - **Navigation commands** — one per module, generated from the `MODULES` config array. Each calls `navigate(module.path)` and closes the palette.\n   - **Theme commands** — one per theme option, generated from `THEME_OPTIONS`. Each calls `handleThemeChange()`.\n   - **Demo commands** — \"Open Federation Lab\", per-module kill/restore toggles (label flips based on `killed[id]`), and a deployment ring toggle (label flips based on `variant`).\n\n2. **Filtering** — `filteredCommands` is a `useMemo` that runs a case-insensitive substring match against each command's `title + subtitle + keywords` string. The `keywords` field contains aliases (e.g., \"module navigation 3001\") so searching by port number or concept works.\n\n3. **Keyboard binding** — a `keydown` listener in `ShellFrame` catches `Ctrl+K` / `Cmd+K` to open, `Escape` to close. The query resets to empty when the palette closes.\n\n4. **Component structure** — the `CommandPalette` component renders a backdrop, a search input (auto-focused via `useRef`), and a scrollable list of command buttons. Each button calls `command.run()` which performs the action and closes the palette.\n\n```tsx\n// Command definition — every action is data-driven\nconst commandActions = useMemo\u003creadonly CommandAction[]\u003e(() =\u003e {\n  const navigationCommands = MODULES.map((module) =\u003e ({\n    id: `goto-${module.id}`,\n    title: `Switch to ${module.label}`,\n    subtitle: `Load the ${module.label.toLowerCase()} micro-frontend on port ${module.port}`,\n    keywords: `${module.id} ${module.label.toLowerCase()} module navigation ${module.port}`,\n    run: () =\u003e { navigate(module.path); setIsCommandPaletteOpen(false); },\n  }));\n\n  const demoCommands: CommandAction[] = [\n    {\n      id: \"demo-panel\",\n      title: \"Open Federation Lab\",\n      keywords: \"demo lab federation health kill fault isolation version canary\",\n      run: () =\u003e { setIsDemoPanelOpen(true); setIsCommandPaletteOpen(false); },\n    },\n    ...MODULES.map((module) =\u003e ({\n      id: `kill-${module.id}`,\n      title: `${killed[module.id] ? \"Restore\" : \"Kill\"} ${module.label} Remote`,\n      run: () =\u003e { toggleKill(module.id); setIsCommandPaletteOpen(false); },\n    })),\n  ];\n\n  return [...navigationCommands, ...themeCommands, ...demoCommands];\n}, [navigate, killed, variant]);\n\n// Filtering — simple substring match against a combined search string\nconst filteredCommands = useMemo(() =\u003e {\n  const q = commandQuery.trim().toLowerCase();\n  if (!q) return commandActions;\n  return commandActions.filter((cmd) =\u003e\n    `${cmd.title} ${cmd.subtitle} ${cmd.keywords}`.toLowerCase().includes(q)\n  );\n}, [commandActions, commandQuery]);\n```\n\n### Lab (Federation Lab)\n\n**Purpose:** A live demo control panel for proving micro-frontend resilience — fault isolation, health monitoring, and independent deployment versioning.\n\nClick the **Lab** button (orange border, right side of header) or use the command palette (`Ctrl+K` → \"Open Federation Lab\") to open a full-height slide-over panel. It contains four sections:\n\n**1. Remote Health Monitor**\n- Polls each remote's `remoteEntry.js` endpoint every 5 seconds (only while the panel is open).\n- Shows a color-coded status dot (green = online, red = offline/killed, gray = checking) with per-remote latency in milliseconds.\n- Lists each remote by name, port, and current status.\n\n**2. Fault Isolation — Kill Switches**\n- One toggle per remote module. Clicking a toggle \"kills\" that remote — the shell immediately renders a `ModuleFallback` component for that module while all other modules continue working.\n- **Kill All** / **Restore All** bulk actions for dramatic demo moments.\n- The kill is client-side only (the dev server keeps running). It simulates what happens when a remote's CDN is down or a deploy is broken.\n- The status strip in the header shows a red \"N KILLED\" counter when any remotes are down.\n\n**3. A/B Deployment Ring**\n- Toggles between **Stable** and **Canary** deployment variants.\n- Shows per-module version info (version number + build hash) that changes based on the active ring.\n- The status strip shows a \"CANARY\" badge when the canary ring is active.\n- Demonstrates how each remote can be deployed at a different version independently — one team ships canary while others stay on stable.\n\n**4. Hot Reload Guide**\n- Step-by-step instructions for demonstrating independent deployment live: stop a remote's dev server, show the ErrorBoundary fallback, edit source code, restart the server, click Retry — the module reloads with changes while others never went down.\n\n**Why it matters for the demo:** This is the centerpiece of the live talk. It lets the speaker prove fault isolation in real time (kill a remote → others keep running), show independent versioning (canary ring), and demonstrate that the architecture handles failure gracefully. It answers the skeptic's question: \"What happens when one team's deploy breaks?\"\n\n**Implementation (`lib/health.ts` + `lib/demo.ts` + `DemoPanel.tsx`):**\n\nThe Federation Lab is composed from three custom hooks in `lib/` and one presentation component:\n\n**1. `useRemoteHealth(remotes, enabled)` — `lib/health.ts`**\n\nPolls each remote's `remoteEntry.js` via `fetch()` with `method: \"HEAD\"` and `mode: \"no-cors\"` every 5 seconds. Returns a readonly array of `RemoteHealth` objects with `status` (`\"online\" | \"offline\" | \"checking\"`), `latencyMs`, and `lastChecked`. Polling only runs when `enabled` is `true` (tied to the panel being open) to avoid unnecessary network traffic. Uses `useRef` for the enabled flag to avoid stale closures in the interval callback.\n\n```typescript\n// lib/health.ts — core polling logic\nasync function checkRemote(port: string): Promise\u003c{ ok: boolean; latencyMs: number }\u003e {\n  const start = performance.now();\n  try {\n    const response = await fetch(`http://localhost:${port}/remoteEntry.js`, {\n      method: \"HEAD\",\n      mode: \"no-cors\",\n      cache: \"no-store\",\n    });\n    const latencyMs = Math.round(performance.now() - start);\n    return { ok: response.ok || response.type === \"opaque\", latencyMs };\n  } catch {\n    return { ok: false, latencyMs: Math.round(performance.now() - start) };\n  }\n}\n```\n\n**2. `useKillSwitch(moduleIds)` — `lib/demo.ts`**\n\nManages a `Record\u003cstring, boolean\u003e` of killed states. Returns `{ killed, toggle, killAll, restoreAll }`. The `toggle` function flips one module's killed state; `killAll`/`restoreAll` set all modules at once. State is entirely client-side — no server interaction. The shell's `ModuleView` component checks `isKilled` before rendering: if true, it renders `\u003cModuleFallback\u003e` immediately instead of attempting the lazy import.\n\n```typescript\n// lib/demo.ts — kill switch hook\nexport function useKillSwitch(moduleIds: readonly string[]) {\n  const [killed, setKilled] = useState\u003cKilledRemotes\u003e(() =\u003e\n    Object.fromEntries(moduleIds.map((id) =\u003e [id, false]))\n  );\n\n  const toggle = useCallback((id: string) =\u003e {\n    setKilled((prev) =\u003e ({ ...prev, [id]: !prev[id] }));\n  }, []);\n\n  const killAll = useCallback(() =\u003e {\n    setKilled((prev) =\u003e\n      Object.fromEntries(Object.keys(prev).map((id) =\u003e [id, true]))\n    );\n  }, []);\n\n  const restoreAll = useCallback(() =\u003e {\n    setKilled((prev) =\u003e\n      Object.fromEntries(Object.keys(prev).map((id) =\u003e [id, false]))\n    );\n  }, []);\n\n  return { killed, toggle, killAll, restoreAll } as const;\n}\n\n// App.tsx — how the kill switch integrates with rendering\nfunction ModuleView({ module, isKilled }: { module: ModuleConfig; isKilled: boolean }) {\n  if (isKilled) {\n    return \u003cModuleFallback title={`${module.label} Module Killed`} message=\"...\" /\u003e;\n  }\n  return (\n    \u003cErrorBoundary\u003e\n      \u003cSuspense fallback={\u003cSkeleton /\u003e}\u003e\n        \u003cComponent /\u003e\n      \u003c/Suspense\u003e\n    \u003c/ErrorBoundary\u003e\n  );\n}\n```\n\n**3. `useVersionRegistry(moduleIds)` — `lib/demo.ts`**\n\nManages a `\"stable\" | \"canary\"` variant toggle with two static version maps (`MOCK_VERSIONS` and `CANARY_VERSIONS`). Returns `{ variant, versions, toggleVariant }`. The `versions` array is a `useMemo` that selects from the active registry. In a real production system, these version maps would come from a remote manifest or deployment API.\n\n```typescript\n// lib/demo.ts — version registry hook\nexport function useVersionRegistry(moduleIds: readonly string[]) {\n  const [variant, setVariant] = useState\u003cDeploymentVariant\u003e(\"stable\");\n\n  const versions = useMemo\u003creadonly RemoteVersionInfo[]\u003e(() =\u003e {\n    const registry = variant === \"stable\" ? MOCK_VERSIONS : CANARY_VERSIONS;\n    return moduleIds.map((id) =\u003e registry[id]!);\n  }, [moduleIds, variant]);\n\n  const toggleVariant = useCallback(() =\u003e {\n    setVariant((prev) =\u003e (prev === \"stable\" ? \"canary\" : \"stable\"));\n  }, []);\n\n  return { variant, versions, toggleVariant } as const;\n}\n```\n\n**4. `DemoPanel.tsx` — presentation component**\n\nA `memo`-wrapped component that receives all three hooks' outputs as props. It renders four sections (Health Monitor, Kill Switches, A/B Deployment, Hot Reload Guide) as pure presentation with zero business logic. Status colors are driven by a `STATUS_CONFIG` lookup table. The component is entirely controlled — all state mutations happen through callback props (`onToggleKill`, `onKillAll`, `onRestoreAll`, `onToggleVariant`).\n\n### Individual kill scripts\n\n```bash\npnpm run kill:records    # Stop records on :3001\npnpm run kill:prescriptions         # Stop prescriptions on :3002\npnpm run kill:analytics    # Stop analytics on :3003\npnpm run kill:home         # Stop home on :3004\npnpm run kill:ports        # Stop all demo ports (3000–3004)\n```\n\n## Testing\n\nThe project uses **Vitest** + **React Testing Library** with `jsdom` for component testing. Tests live alongside source files.\n\n```bash\npnpm test              # Run all tests once\npnpm run test:watch    # Watch mode\npnpm run test:coverage # Coverage report (v8)\npnpm run lint          # Lint all packages with ESLint\npnpm run typecheck     # TypeScript validation across all packages\n```\n\nRemote module imports are aliased in `vitest.config.ts` so federated components can be tested in isolation without running dev servers. Each package has its own test file:\n\n- `packages/shell/src/App.test.tsx` — navigation, tab switching, notification system, skeleton fallbacks\n- `packages/records/src/MedicalRecords.test.tsx` — filtering, add-to-cart events, records grid\n- `packages/prescriptions/src/PrescriptionOrders.test.tsx` — quantity controls, remove items, order summary, event listeners\n- `packages/analytics/src/ClinicalAnalytics.test.tsx` — stats display, activity stream, welcome banner\n\nThe shell test suite also covers theme restoration from `localStorage`, theme persistence, and `themeChange` event broadcasting.\n\n## GitHub Pages Deployment\n\nThis repo includes [.github/workflows/deploy.yml](.github/workflows/deploy.yml), which builds all remotes, assembles a single static site, and deploys it with the GitHub Pages artifact workflow.\n\nThe workflow now checks whether a Pages site already exists before building. If the repository has never had Pages enabled, there are two supported bootstrap paths:\n\n- Enable Pages once in the repository settings and set **Source** to **GitHub Actions**.\n- Or add a `PAGES_ADMIN_TOKEN` repository secret with **Pages: write** and **Administration: write** permissions so the workflow can create the Pages site automatically.\n\nAfter that first bootstrap, normal deploys can continue with the default workflow token.\n\n## Per-Module CI Pipelines (Independent Build \u0026 Deploy)\n\nEach micro-frontend has its own GitHub Actions workflow that triggers **only when that module's code changes**:\n\n| Module | Workflow | Triggers on |\n|--------|----------|-------------|\n| Shell | `ci-shell.yml` | `packages/shell/**` |\n| Home | `ci-home.yml` | `packages/home/**` |\n| Records | `ci-records.yml` | `packages/records/**` |\n| Prescriptions | `ci-prescriptions.yml` | `packages/prescriptions/**` |\n| Analytics | `ci-analytics.yml` | `packages/analytics/**` |\n\nEach workflow runs four parallel-then-gated jobs:\n\n```\nlint ──┐\ntypecheck ──┼──► build (uploads artifact)\ntest ──┘\n```\n\nAll workflows also trigger when shared root configs change (`vitest.config.ts`, `eslint.config.mjs`, `package.json`).\n\nThe full-repo [ci.yml](.github/workflows/ci.yml) still exists as a safety net for cross-cutting changes, but in a real multi-repo setup the per-module workflows are all you need.\n\n### Why per-module workflows matter\n\nThis is the **independent deploy** promise of micro-frontends in action:\n\n- **Records team** pushes a fix → only `ci-records.yml` runs → only Records is linted, typechecked, tested, and built\n- **Shell team** pushes a feature → only `ci-shell.yml` runs → other modules are untouched\n- A PR that touches `packages/prescriptions/` does NOT trigger CI for analytics, records, or home\n- Each module's build artifact is uploaded independently and can be deployed to its own CDN/S3 bucket\n\nIn a production multi-repo setup, each of these workflows would live in its own repository and deploy to its own origin. The shell discovers remotes at runtime via `remoteEntry.js` URLs — it never needs to build the remotes itself.\n\n## React Suspense Streaming Pattern\n\nEach remote uses a Resource-based Suspense pattern to simulate network streaming:\n\n```typescript\nfunction createResource\u003cT\u003e(asyncFn: () =\u003e Promise\u003cT\u003e): Resource\u003cT\u003e {\n  let status = \"pending\";\n  let result: T;\n  let suspender = asyncFn().then(\n    (data) =\u003e { status = \"success\"; result = data; },\n    (error) =\u003e { status = \"error\"; result = error; }\n  );\n  return {\n    read() {\n      if (status === \"pending\") throw suspender;     // Suspense catches this\n      if (status === \"error\") throw result;           // ErrorBoundary catches this\n      return result;\n    },\n  };\n}\n```\n\nThe shell wraps each lazy-loaded remote in `\u003cSuspense fallback={\u003cSkeleton /\u003e}\u003e` and `\u003cErrorBoundary\u003e`, giving each module independent loading and error states. The shell uses three distinct loading strategies:\n\n- **Instant** (Home) — imports the standalone component via `home/Home`, no streaming delay\n- **Eager** (Records) — imports the streaming wrapper but preloads it on shell mount\n- **Streamed** (Prescriptions, Analytics) — loaded on demand with skeleton fallbacks\n\n## Why React 19 (Not 18) for Module Federation + Suspense\n\nReact 19 introduced a behavioral change in Suspense: sibling components inside the **same** `\u003cSuspense\u003e` boundary now render sequentially instead of in parallel. If the first sibling suspends, subsequent siblings wait — creating a potential waterfall. This raised concerns in the community about whether React 19 is safe for streaming micro-frontends.\n\n**This architecture is unaffected.** Here's why:\n\n| Concern | This demo's architecture | Impact |\n|---------|--------------------------|--------|\n| Sibling waterfall | Route-based rendering — only **one** module renders at a time | Not affected |\n| Same-boundary siblings | Each module has its own `\u003cSuspense\u003e` + `\u003cErrorBoundary\u003e` | Parallel preserved |\n| throw-promise pattern | `createResource` still works in React 19 (legacy, not broken) | No breakage |\n| Pre-fetching | Eager preload + hover prefetch = chunks cached before render | Waterfall impossible |\n\n**React 19 actively benefits this architecture:**\n\n1. **Suspense batching (19.2+)** — Instead of showing fallbacks one boundary at a time, React 19 groups multiple boundary transitions in a single render pass. This means skeleton → content transitions are smoother — no \"popping in\" effect when navigating between modules.\n\n2. **Render-as-you-fetch** — React 19 encourages hoisting data calls outside components. The `createResource` pattern already does this — the resource is created at module evaluation time, not inside the component. The component just calls `resource.read()`.\n\n3. **Deterministic concurrent rendering** — The shell re-renders frequently (theme changes, command palette filtering, kill switch toggles). React 19's compiler optimizations skip entire update paths that haven't changed, making these interactions snappier.\n\n4. **Streaming SSR readiness** — React 19's improved batching in streaming SSR reduces \"UI churn\" (flickering). If this demo ever adds SSR, the skeleton → content transitions would look even better server-side.\n\n```tsx\n// This architecture avoids the waterfall by design:\n\n// 1. Route-based: only one module renders at a time\n\u003cRoutes\u003e\n  \u003cRoute path=\"/records\" element={\u003cModuleView module={records} /\u003e} /\u003e\n  \u003cRoute path=\"/prescriptions\" element={\u003cModuleView module={prescriptions} /\u003e} /\u003e\n\u003c/Routes\u003e\n\n// 2. Each module has its own Suspense boundary (never siblings)\nfunction ModuleView({ module }) {\n  return (\n    \u003cErrorBoundary\u003e           {/* ← own error boundary */}\n      \u003cSuspense fallback={..}\u003e {/* ← own suspense boundary */}\n        \u003cmodule.component /\u003e\n      \u003c/Suspense\u003e\n    \u003c/ErrorBoundary\u003e\n  );\n}\n\n// 3. Pre-fetching eliminates any remaining concern\nconst EAGER_MODULES = MODULES.filter((m) =\u003e m.loadStrategy === \"eager\");\nfor (const m of EAGER_MODULES) { PREFETCHERS[m.id](); }  // cached before render\n```\n\n\u003e **Bottom line:** React 19 is the right choice. The waterfall concern applies to sibling components in the same boundary — a pattern this architecture intentionally avoids. The batching and compiler improvements directly benefit the shell's UX.\n\n## Microservices vs Micro-frontends — Fault Isolation\n\nA common question: \"Is this like microservices where one broken service doesn't take down the others?\" **Yes — but with a nuance.**\n\n| | Microservices | This project (Micro-frontends) |\n|---|---|---|\n| **Isolation boundary** | Separate processes/containers — OS-level | Separate `ErrorBoundary` per module — React-level |\n| **If one crashes** | Other services keep running (OS guarantee) | Other modules keep rendering (ErrorBoundary catches the error) |\n| **Blast radius** | Network call fails, caller handles it | `import()` fails or component throws, ErrorBoundary shows fallback + Retry |\n| **Shared resource risk** | Each service has its own memory/CPU | All modules share one browser tab |\n\n### What this project does to achieve isolation\n\n1. **Per-module `ErrorBoundary`** — every remote is wrapped individually in `ModuleView`. If Records crashes, Prescriptions and Analytics keep working.\n2. **Per-module `Suspense`** — each remote has its own loading state. A slow remote only shows *its own* skeleton.\n3. **Route-based rendering** — only one module renders at a time, so a broken remote can't corrupt another module's DOM.\n4. **Independent deployment** — each remote has its own build and `remoteEntry.js`. A broken Records deploy doesn't touch Prescriptions.\n5. **`.catch()` on `lazy()`** — if a remote's `remoteEntry.js` fails to load (server down, network error), the import resolves to a `ModuleFallback` instead of crashing.\n\n### The one gap vs microservices\n\nAll modules share one browser tab. If a remote has an infinite loop or massive memory leak, it freezes the entire page. Microservices don't have this problem because each runs in its own process. The fix is `\u003ciframe\u003e` isolation, but that breaks shared React context and degrades DX. Most teams accept this tradeoff — and it's why code review and testing at the module level matter.\n\n## Conference Demo Value\n\nThis project demonstrates these micro-frontend concepts during a live talk:\n\n1. **Independent deployment** — each remote starts on its own port with its own build\n2. **Fault isolation** — kill a remote server and only that module shows a fallback (or use the Federation Lab kill switch)\n3. **Shared dependencies** — React is loaded once via singleton sharing\n4. **Suspense streaming** — skeleton screens appear during module load, then content streams in (for streamed and eager modules)\n5. **Loading strategy taxonomy** — instant (Home), eager (Records), streamed (Prescriptions/Analytics) — not every module should load the same way\n5. **Loose coupling** — modules communicate through events, not imports\n6. **Host-owned routing** — remotes can request navigation through `navigateToModule` without importing `react-router-dom`\n7. **Independent tech choices** — each package has its own `rspack.config.ts`, `postcss.config.cjs`, and `tsconfig.json`\n8. **Design system consistency** — shared `@theme` tokens across all packages keep the UI cohesive without a shared CSS build step\n9. **Live demo controls** — the Federation Lab panel lets you kill/restore remotes, monitor health, and toggle A/B deployment during a presentation\n\n### What to show in a talk\n\n- Start `pnpm run dev`, open `:3000` — Home loads **instantly** (no skeleton delay, status strip shows INSTANT)\n- Click Records — loads fast because it was **eagerly preloaded** on shell mount (status strip shows EAGER)\n- Click Prescriptions — observe skeleton **streaming** in (status strip shows STREAMING)\n- Navigate to `/records`, add a prescription, then use the prescriptions empty-state CTA to show remote-requested host navigation\n- Open the Federation Lab (click **Lab** in the header) and kill the records remote — records shows `ModuleFallback`, prescriptions and analytics continue working\n- Restore the remote from the Lab panel — records comes back\n- Toggle the A/B deployment ring from stable to canary — version info updates per module\n- Kill a real remote server (`Ctrl+C` on `:3001`) — the health monitor detects it offline\n- Restart it — records comes back without refreshing the shell\n- Inspect the network tab — each module loads its own `remoteEntry.js` chunk\n\n## Scripts\n\n| Command | Description |\n|---------|-------------|\n| `pnpm run dev` | Start all five dev servers concurrently |\n| `pnpm run build` | Build all five packages for production |\n| `pnpm run dev:shell` | Start only the shell (`:3000`) |\n| `pnpm run dev:home` | Start only home (`:3004`) |\n| `pnpm run dev:records` | Start only records (`:3001`) |\n| `pnpm run dev:prescriptions` | Start only prescriptions (`:3002`) |\n| `pnpm run dev:analytics` | Start only analytics (`:3003`) |\n| `pnpm run kill:ports` | Kill all demo ports (`3000`–`3004`) |\n| `pnpm run kill:records` | Kill only the records port (`:3001`) |\n| `pnpm run kill:prescriptions` | Kill only the prescriptions port (`:3002`) |\n| `pnpm run kill:analytics` | Kill only the analytics port (`:3003`) |\n| `pnpm run kill:home` | Kill only the home port (`:3004`) |\n\n## Prerequisites\n\n- Node.js 20+ (required for Tailwind CSS v4)\n- pnpm 9+\n- Modern browser (Chrome 111+, Firefox 128+, Safari 16.4+)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlinduldulao%2Fmodule-federation-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevlinduldulao%2Fmodule-federation-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlinduldulao%2Fmodule-federation-demo/lists"}