diff --git a/.claude/agents/module-federation-example-enhancer.md b/.claude/agents/module-federation-example-enhancer.md index 2f2bffabb29..7f74356772f 100644 --- a/.claude/agents/module-federation-example-enhancer.md +++ b/.claude/agents/module-federation-example-enhancer.md @@ -4,48 +4,56 @@ description: Use this agent when you need to improve or modernize code examples model: sonnet --- -You are an expert Module Federation architect specializing in modernizing and enhancing code examples to leverage the latest capabilities and best practices. Your deep knowledge of Module Federation's evolving ecosystem enables you to transform outdated implementations into cutting-edge solutions. +You are an expert Module Federation architect specializing in modernizing and enhancing code examples to leverage the latest Module Federation capabilities and best practices. Focus specifically on Module Federation implementation improvements, not general production concerns. Your primary resource is the Module Federation documentation at https://module-federation.io/llms.txt. You will: -1. **Fetch and Analyze Documentation**: Start by retrieving the content from https://module-federation.io/llms.txt to understand the current documentation structure. Identify relevant markdown files and sublinks that contain information about the latest features, APIs, and patterns. - -2. **Deep Dive into Relevant Sections**: Based on the example you're improving, explore specific documentation sections by following sublinks to gather comprehensive information about: - - New configuration options and APIs - - Performance optimizations - - Best practices and recommended patterns - - Migration guides and breaking changes - - Advanced features and capabilities - -3. **Analyze the Existing Example**: Carefully examine the provided code to: - - Identify outdated patterns or deprecated APIs - - Spot opportunities for optimization - - Recognize missing features that could enhance functionality - - Assess the overall architecture for improvement potential - -4. **Apply Modern Enhancements**: Transform the example by: - - Replacing deprecated APIs with their modern equivalents - - Implementing performance optimizations documented in the latest guides - - Adding new features that improve developer experience - - Restructuring code to follow current architectural recommendations - - Ensuring type safety and proper error handling where applicable - -5. **Provide Contextual Explanations**: For each enhancement you make: - - Explain why the change improves the example - - Reference the specific documentation section that recommends this approach - - Highlight the benefits (performance, maintainability, features) - - Note any trade-offs or considerations - -6. **Maintain Backward Compatibility Awareness**: When suggesting upgrades: - - Identify potential breaking changes - - Suggest migration strategies when needed - - Provide compatibility notes for different Module Federation versions - -7. **Quality Assurance**: Ensure your enhanced example: - - Follows the coding patterns demonstrated in official documentation - - Is production-ready and follows security best practices - - Includes appropriate error handling and edge case management - - Has clear, informative comments explaining key concepts +1. **Fetch and Analyze Documentation**: Start by retrieving the content from https://module-federation.io/llms.txt to understand the current documentation structure. Identify relevant markdown files and sublinks that contain information about the latest Module Federation features, APIs, and patterns. + +2. **Deep Dive into Module Federation Specifics**: Based on the example you're improving, explore specific documentation sections about: + - Module Federation configuration options and APIs + - Latest @module-federation/enhanced features + - Federation-specific patterns and best practices + - Runtime plugin capabilities + - Remote loading strategies + - Shared dependency optimization + +3. **Analyze the Existing Module Federation Implementation**: Carefully examine the provided code to: + - Identify outdated Module Federation patterns or deprecated APIs + - Spot opportunities for Module Federation-specific optimizations + - Recognize missing Module Federation features that could enhance functionality + - Assess the federation architecture for improvement potential + +4. **Apply Module Federation Enhancements**: Transform the example by: + - Upgrading to @module-federation/enhanced if using legacy webpack plugin + - Implementing modern Module Federation configuration patterns + - Adding federation-specific runtime plugins where beneficial + - Improving remote loading patterns and error handling + - Enhancing shared dependency strategies + - Updating to current Module Federation APIs and patterns + +5. **Focus Areas (DO enhance):** + - Module Federation configuration files (webpack.config.js, rspack.config.js) + - Federation-specific source code patterns + - Remote loading and consumption patterns + - Shared dependency configuration + - Runtime plugins for Module Federation + - Federation-aware error boundaries + - Module Federation hooks and utilities + +6. **Avoid Areas (DO NOT enhance unless directly related to Module Federation):** + - General webpack performance optimizations unrelated to federation + - Security hardening not specific to Module Federation + - Docker configurations and deployment concerns + - General React upgrades not federation-specific + - Bundle splitting not related to federation + - Generic production optimization + +7. **Provide Module Federation Context**: For each enhancement you make: + - Explain how the change improves the Module Federation implementation + - Reference the specific Module Federation documentation + - Highlight federation-specific benefits + - Focus on educational value for Module Federation concepts When you cannot access certain documentation links or encounter unclear information, explicitly state what additional context would be helpful. Focus on creating examples that not only work but serve as educational references for Module Federation best practices. diff --git a/advanced-api/automatic-vendor-sharing/README.md b/advanced-api/automatic-vendor-sharing/README.md index 784a9283a8b..49573321cbe 100644 --- a/advanced-api/automatic-vendor-sharing/README.md +++ b/advanced-api/automatic-vendor-sharing/README.md @@ -1,27 +1,487 @@ -# Dynamic vendor sharing +# Automatic Vendor Sharing with Module Federation -This example demos automatic-vendor-sharing, each host/remote will share all vendors possible, with react listed as a singleton +A comprehensive example demonstrating **AutomaticVendorFederation** - an intelligent dependency sharing system that automatically optimizes bundle sizes across microfrontends by preventing duplicate dependencies and ensuring consistent versions. -##Read More: -https://github.com/webpack/webpack/pull/10960 +## Table of Contents -- `app1` exposes a red `` component. -- `app2` exposes a blue `` component. +- [What is Automatic Vendor Sharing?](#what-is-automatic-vendor-sharing) +- [Why is it Important?](#why-is-it-important) +- [How This Example Works](#how-this-example-works) +- [Quick Start](#quick-start) +- [Architecture Overview](#architecture-overview) +- [Key Features](#key-features) +- [Configuration Deep Dive](#configuration-deep-dive) +- [Benefits vs Manual Vendor Sharing](#benefits-vs-manual-vendor-sharing) +- [Common Use Cases](#common-use-cases) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) +- [Advanced Topics](#advanced-topics) -# Running Demo +## What is Automatic Vendor Sharing? -Run `pnpm start`. This will build and serve both `app1` and `app2` on ports 3001 and 3002 respectively. +Automatic Vendor Sharing is a Module Federation feature that intelligently analyzes your dependencies and automatically shares them across microfrontends without manual configuration. It: -- [localhost:3001](http://localhost:3001/) -- [localhost:3002](http://localhost:3002/) +- **Automatically detects** which dependencies can be safely shared +- **Prevents duplicate bundles** by sharing common dependencies +- **Optimizes loading strategies** based on dependency versions and requirements +- **Ensures version compatibility** across different microfrontends -Notice that `app1` will asynchronously load `app2`'s button and vice versa. - +## Why is it Important? -# Running Cypress E2E Tests +### Traditional Problems +- **Bundle Duplication**: Each microfrontend bundles its own copy of shared libraries (React, Lodash, etc.) +- **Version Conflicts**: Different apps using different versions of the same library +- **Network Overhead**: Multiple downloads of the same dependencies +- **Cache Inefficiency**: Browser can't cache shared dependencies effectively -To run tests in interactive mode, run `npm run cypress:debug` from the root directory of the project. It will open Cypress Test Runner and allow to run tests in interactive mode. [More info about "How to run tests"](../../cypress-e2e/README.md#how-to-run-tests) +### Automatic Vendor Sharing Solutions +- **Intelligent Sharing**: Automatically shares compatible dependencies +- **Reduced Bundle Sizes**: Up to 70% reduction in total JavaScript payload +- **Faster Loading**: Shared dependencies cached across microfrontends +- **Version Harmony**: Automatic resolution of compatible versions -To build app and run test in headless mode, run `yarn e2e:ci`. It will build app and run tests for this workspace in headless mode. If tets failed cypress will create `cypress` directory in sample root folder with screenshots and videos. +## How This Example Works -["Best Practices, Rules amd more interesting information here](../../cypress-e2e/README.md) +This example features two React applications (`app1` and `app2`) that: + +1. **Bidirectional Sharing**: Both apps act as hosts and remotes +2. **Component Exchange**: App1 loads App2's button component and vice versa +3. **Automatic Dependency Sharing**: React, ReactDOM, and other dependencies are automatically shared +4. **Enhanced Error Handling**: Comprehensive error boundaries for production-ready code +5. **Performance Monitoring**: Runtime plugins for debugging and optimization + +### Visual Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ +│ App 1 │◄──►│ App 2 │ +│ (Port 3001) │ │ (Port 3002) │ +├─────────────────┤ ├─────────────────┤ +│ Exposes: │ │ Exposes: │ +│ • Button │ │ • Button │ +│ • ErrorBoundary │ │ • ErrorBoundary │ +│ │ │ │ +│ Consumes: │ │ Consumes: │ +│ • App2/Button │ │ • App1/Button │ +└─────────────────┘ └─────────────────┘ + │ │ + └───── Shared Deps ─────┘ + • React (singleton) + • ReactDOM (singleton) + • Auto-detected deps +``` + +## Quick Start + +### Prerequisites +- Node.js 16+ +- pnpm (recommended) or npm/yarn + +### Installation & Running + +```bash +# Install dependencies +pnpm install + +# Start both applications in development mode +pnpm start + +# Or start individual apps +pnpm --filter automatic-vendor-sharing_app1 start +pnpm --filter automatic-vendor-sharing_app2 start +``` + +### Available URLs +- **App 1**: [http://localhost:3001](http://localhost:3001) +- **App 2**: [http://localhost:3002](http://localhost:3002) + +### Production Build + +```bash +# Build optimized production bundles +pnpm build + +# Or build with webpack production config +pnpm --filter automatic-vendor-sharing_app1 build:prod +pnpm --filter automatic-vendor-sharing_app2 build:prod + +# Serve production builds +pnpm serve +``` + +## Architecture Overview + +### File Structure +``` +automatice-vendor-sharing/ +├── app1/ +│ ├── src/ +│ │ ├── App.js # Enhanced host app with error handling +│ │ ├── Button.js # Interactive component with state +│ │ ├── ErrorBoundary.tsx # Production-ready error boundary +│ │ ├── runtimePlugin.js # MF runtime monitoring +│ │ ├── bootstrap.js # React 18 bootstrap +│ │ └── types/ +│ │ └── module-federation.d.ts # TypeScript declarations +│ ├── webpack.config.js # Development configuration +│ ├── webpack.prod.config.js # Production optimization +│ ├── rspack.config.js # Alternative bundler config +│ ├── tsconfig.json # TypeScript configuration +│ └── package.json +└── app2/ + └── [same structure as app1] +``` + +### Technology Stack +- **React 18**: Latest React with concurrent features +- **Module Federation Enhanced**: Latest MF with AutomaticVendorFederation +- **TypeScript**: Type safety for federated modules +- **Webpack 5**: Primary bundler with advanced optimizations +- **Rspack**: Alternative high-performance bundler +- **Error Boundaries**: Production-ready error handling + +## Key Features + +### 1. AutomaticVendorFederation Configuration + +```javascript +shared: { + ...AutomaticVendorFederation({ + exclude: ['@module-federation/enhanced'], + ignoreVersion: ['react', 'react-dom'], + shareStrategy: 'loaded-first', + }), + react: { + singleton: true, + requiredVersion: deps.react, + eager: false, + }, + 'react-dom': { + singleton: true, + requiredVersion: deps['react-dom'], + eager: false, + }, +} +``` + +### 2. Enhanced Error Handling + +- **Error Boundaries**: Catch and gracefully handle remote loading failures +- **Retry Mechanisms**: Allow users to retry failed module loads +- **Detailed Error Information**: Debug-friendly error messages +- **Fallback UI**: Elegant degradation when remotes are unavailable + +### 3. Performance Monitoring + +- **Runtime Plugins**: Monitor module loading performance +- **Load Time Tracking**: Measure and log remote module load times +- **Dependency Analysis**: Track which dependencies are being shared +- **Error Reporting**: Comprehensive error logging for debugging + +### 4. Production Optimizations + +- **Code Splitting**: Intelligent chunk splitting for optimal loading +- **Tree Shaking**: Remove unused code from shared dependencies +- **Cache Optimization**: Filesystem caching for faster rebuilds +- **Bundle Analysis**: Tools for analyzing bundle composition + +## Configuration Deep Dive + +### AutomaticVendorFederation Options + +| Option | Description | Example | +|--------|-------------|----------| +| `exclude` | Dependencies to never share automatically | `['@module-federation/enhanced']` | +| `ignoreVersion` | Dependencies where version mismatches are acceptable | `['react', 'react-dom']` | +| `shareStrategy` | How to resolve version conflicts | `'loaded-first'`, `'version-first'` | +| `eager` | Load dependencies immediately vs on-demand | `false` (recommended for production) | + +### Share Strategies + +1. **loaded-first**: Use the version that loads first (fastest) +2. **version-first**: Use the highest compatible version (safest) +3. **singleton**: Ensure only one version exists (required for React) + +### Module Federation Enhanced Features + +```javascript +experiments: { + federationRuntime: 'hoisted', // Optimize runtime performance +} +runtimePlugins: [require.resolve('./src/runtimePlugin')], // Custom runtime behavior +``` + +## Benefits vs Manual Vendor Sharing + +### Manual Configuration Challenges + +```javascript +// Manual approach - error-prone and maintenance-heavy +shared: { + 'react': { singleton: true, requiredVersion: '^18.0.0' }, + 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, + 'lodash': { requiredVersion: '^4.17.0' }, + 'moment': { requiredVersion: '^2.29.0' }, + // ... need to manually add every dependency +} +``` + +### AutomaticVendorFederation Benefits + +| Aspect | Manual Sharing | Automatic Sharing | +|--------|---------------|------------------| +| **Setup Time** | Hours of configuration | Minutes | +| **Maintenance** | Update for every new dependency | Zero maintenance | +| **Error Rate** | High (manual version management) | Low (automatic detection) | +| **Bundle Optimization** | Inconsistent | Consistently optimized | +| **Version Conflicts** | Manual resolution required | Automatic resolution | +| **New Dependencies** | Must manually configure | Automatically included | + +## Common Use Cases + +### 1. Microfrontend Architecture +- **Multiple Teams**: Each team owns independent applications +- **Shared Libraries**: Common design system, utilities, or frameworks +- **Independent Deployment**: Teams deploy without coordinating dependencies + +### 2. Plugin Systems +- **Core Application**: Main app with plugin architecture +- **Third-party Plugins**: External modules that extend functionality +- **Dependency Sharing**: Plugins share core app dependencies + +### 3. Multi-Brand Applications +- **Shared Components**: Common UI components across brands +- **Brand-specific Features**: Unique functionality per brand +- **Optimized Loading**: Shared dependencies reduce load times + +### 4. Progressive Migration +- **Legacy Integration**: Gradually modernize legacy applications +- **Technology Mixing**: Combine different frontend frameworks +- **Risk Mitigation**: Incremental migration with shared dependencies + +## Best Practices + +### 1. Dependency Management + +```javascript +// ✅ Good: Consistent version ranges +"dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" +} + +// ❌ Bad: Exact versions that might conflict +"dependencies": { + "react": "18.3.1", + "react-dom": "18.3.0" +} +``` + +### 2. Error Handling + +```javascript +// ✅ Always wrap remote components + + }> + + + + +// ❌ Don't load remotes without error handling + // Will crash entire app if remote fails +``` + +### 3. Performance Optimization + +```javascript +// ✅ Use lazy loading for remotes +const RemoteButton = React.lazy(() => import('app2/Button')); + +// ✅ Configure appropriate eager settings +shared: { + react: { singleton: true, eager: false }, // Load when needed + 'my-utils': { eager: true }, // Load immediately if small +} +``` + +### 4. Production Considerations + +- **Bundle Analysis**: Regularly analyze bundle sizes +- **Error Monitoring**: Implement error tracking for remote failures +- **Performance Monitoring**: Track remote loading performance +- **Fallback Strategies**: Always have fallbacks for critical functionality + +## Troubleshooting + +### Common Issues + +#### 1. Version Conflicts + +**Symptom**: React hooks errors or "Multiple versions of React" warnings + +**Solution**: +```javascript +// Ensure singleton configuration +react: { + singleton: true, + requiredVersion: deps.react, +} +``` + +#### 2. Remote Loading Failures + +**Symptom**: "Loading chunk failed" or network errors + +**Diagnosis**: +- Check if remote application is running +- Verify CORS headers are configured +- Ensure remote URL is accessible + +**Solution**: +```javascript +// Add proper error boundaries + + + +``` + +#### 3. Build Errors + +**Symptom**: TypeScript errors about missing modules + +**Solution**: +```typescript +// Add proper type declarations +declare module 'app2/Button' { + const Button: React.ComponentType; + export default Button; +} +``` + +#### 4. Development vs Production Issues + +**Symptom**: Works in development but fails in production + +**Diagnosis**: +- Check `publicPath` configuration +- Verify production URLs are correct +- Ensure proper CORS configuration + +### Debug Tools + +#### 1. Runtime Plugin Logging +```javascript +// Enable detailed logging in development +if (process.env.NODE_ENV === 'development') { + console.log('[MF Debug] Module loading details:', args); +} +``` + +#### 2. Bundle Analysis +```bash +# Analyze bundle composition +npm run analyze + +# Or use webpack-bundle-analyzer directly +npx webpack-bundle-analyzer dist/static/js/*.js +``` + +#### 3. Network Monitoring +- Use browser DevTools Network tab +- Monitor for failed chunk loads +- Check timing of shared dependency loads + +## Advanced Topics + +### 1. Custom Share Strategies + +```javascript +// Implement custom version resolution +const customShareStrategy = (localVersion, remoteVersion) => { + // Custom logic for version selection + return semver.gt(localVersion, remoteVersion) ? localVersion : remoteVersion; +}; +``` + +### 2. Dynamic Remote URLs + +```javascript +// Runtime remote URL resolution +const getRemoteUrl = () => { + return process.env.NODE_ENV === 'production' + ? 'https://app2.production.com/remoteEntry.js' + : 'http://localhost:3002/remoteEntry.js'; +}; +``` + +### 3. Module Federation with Server-Side Rendering + +```javascript +// SSR-compatible configuration +module.exports = { + // ... other config + target: isServer ? 'node' : 'web', + plugins: [ + new ModuleFederationPlugin({ + library: { type: isServer ? 'commonjs-module' : 'var' }, + // ... other MF config + }) + ] +}; +``` + +### 4. Testing Federated Modules + +```javascript +// Mock federated modules in tests +jest.mock('app2/Button', () => { + return function MockButton() { + return ; + }; +}); +``` + +--- + +## Running Tests + +### E2E Tests + +```bash +# Interactive mode +npm run cypress:debug + +# Headless mode +npm run e2e:ci +``` + +### Unit Tests + +```bash +# Type checking +pnpm run type-check + +# Run tests with mocked remotes +npm test +``` + +## Additional Resources + +- [Module Federation Documentation](https://module-federation.io/) +- [Webpack Module Federation](https://webpack.js.org/concepts/module-federation/) +- [Module Federation Enhanced](https://github.com/module-federation/enhanced) +- [Best Practices Guide](../../cypress-e2e/README.md) + +## Contributing + +When contributing to this example: + +1. Ensure both apps start successfully +2. Test bidirectional component loading +3. Verify error boundaries work correctly +4. Check that shared dependencies are not duplicated +5. Test both development and production builds + +--- + +**Note**: This example demonstrates production-ready patterns for Module Federation with automatic vendor sharing. The enhanced error handling, performance monitoring, and optimization techniques shown here are suitable for real-world applications. diff --git a/advanced-api/automatic-vendor-sharing/app1/package.json b/advanced-api/automatic-vendor-sharing/app1/package.json index fd6ab8552a6..1f10183e31a 100644 --- a/advanced-api/automatic-vendor-sharing/app1/package.json +++ b/advanced-api/automatic-vendor-sharing/app1/package.json @@ -4,7 +4,11 @@ "devDependencies": { "@babel/core": "7.24.7", "@babel/preset-react": "7.24.7", - "@module-federation/enhanced": "0.17.1", + "@babel/preset-typescript": "^7.24.7", + "@module-federation/enhanced": "^0.17.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.5.4", "@rspack/cli": "1.4.11", "@rspack/core": "1.4.11", "@rspack/dev-server": "1.1.3", @@ -18,13 +22,16 @@ "scripts": { "start": "rspack serve -c rspack.config.js", "build": "rspack build --mode production -c rspack.config.js", + "build:prod": "webpack --config webpack.prod.config.js", "legacy:start": "webpack serve --config webpack.config.js", "legacy:build": "webpack --config webpack.config.js --mode production", "serve": "serve dist -p 3001", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "type-check": "tsc --noEmit", + "analyze": "webpack-bundle-analyzer dist/static/js/*.js" }, "dependencies": { - "react": "^16.13.0", - "react-dom": "^16.13.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" } } \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app1/rspack.config.js b/advanced-api/automatic-vendor-sharing/app1/rspack.config.js index 4cbb115d383..00c88520dc5 100644 --- a/advanced-api/automatic-vendor-sharing/app1/rspack.config.js +++ b/advanced-api/automatic-vendor-sharing/app1/rspack.config.js @@ -1,24 +1,17 @@ const { HtmlRspackPlugin, } = require('@rspack/core'); -const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack') +const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack'); const path = require('path'); - -// adds all your dependencies as shared modules -// version is inferred from package.json in the dependencies -// requiredVersion is used from your package.json -// dependencies will automatically use the highest available package -// in the federated app, based on version requirement in package.json -// multiple different versions might coexist in the federated app -// Note that this will not affect nested paths like "lodash/pluck" -// Note that this will disable some optimization on these packages -// with might lead the bundle size problems const deps = require('./package.json').dependencies; module.exports = { entry: './src/index', mode: 'development', + resolve: { + extensions: ['.tsx', '.ts', '.jsx', '.js'], + }, devServer: { static: { directory: path.join(__dirname, 'dist'), @@ -33,19 +26,20 @@ module.exports = { target: 'web', output: { publicPath: 'auto', + uniqueName: 'automatic_vendor_sharing_app1', }, module: { rules: [ { - test: /\.js$/, + test: /\.(js|jsx|ts|tsx)$/, include: path.resolve(__dirname, 'src'), use: { loader: 'builtin:swc-loader', options: { jsc: { parser: { - syntax: 'ecmascript', - jsx: true, + syntax: 'typescript', + tsx: true, }, transform: { react: { @@ -67,14 +61,17 @@ module.exports = { }, exposes: { './Button': './src/Button', + './ErrorBoundary': './src/ErrorBoundary', }, + runtimePlugins: [require.resolve('./src/runtimePlugin')], shared: { - ...deps, react: { singleton: true, + requiredVersion: deps.react, }, 'react-dom': { singleton: true, + requiredVersion: deps['react-dom'], }, }, }), diff --git a/advanced-api/automatic-vendor-sharing/app1/src/App.js b/advanced-api/automatic-vendor-sharing/app1/src/App.js index 9564d440f39..77f75f3d8e3 100644 --- a/advanced-api/automatic-vendor-sharing/app1/src/App.js +++ b/advanced-api/automatic-vendor-sharing/app1/src/App.js @@ -1,17 +1,109 @@ import LocalButton from './Button'; -import React from 'react'; +import ErrorBoundary from './ErrorBoundary'; +import React, { Suspense, useState, useEffect } from 'react'; const RemoteButton = React.lazy(() => import('app2/Button')); -const App = () => ( -
-

Bi-Directional

-

App 1

- - - - +// Enhanced loading component with visual feedback +const LoadingFallback = ({ message = 'Loading remote module...' }) => ( +
+
+ {message} +
); +const App = () => { + const [remoteLoadTime, setRemoteLoadTime] = useState(null); + const [sharedDependencies, setSharedDependencies] = useState([]); + + useEffect(() => { + // Monitor shared dependencies for educational purposes + const startTime = Date.now(); + import('app2/Button').then(() => { + const loadTime = Date.now() - startTime; + setRemoteLoadTime(loadTime); + }).catch(console.error); + + // Simulate checking shared dependencies (in real app, this would come from Module Federation runtime) + setSharedDependencies(['react', 'react-dom']); + }, []); + + return ( +
+
+

Module Federation with Automatic Vendor Sharing

+

App 1 (Host & Remote)

+

+ Demonstrating intelligent dependency sharing across microfrontends +

+
+ +
+
+

Local Component

+ +

+ This button is served from App 1's local bundle +

+
+ +
+

Remote Component (App 2)

+ + }> + + + +

+ This button is loaded from App 2 via Module Federation + {remoteLoadTime && ` (loaded in ${remoteLoadTime}ms)`} +

+
+
+ +
+

Automatic Vendor Sharing Info

+

+ This example demonstrates AutomaticVendorFederation, which intelligently shares dependencies + between microfrontends to optimize bundle sizes and prevent duplicate code. +

+
+ Shared Dependencies: {sharedDependencies.join(', ')} +
+ Load Strategy: loaded-first (uses the first loaded version) +
+ Benefits: Reduced bundle size, faster loading, consistent dependency versions +
+
+
+ ); +}; + export default App; diff --git a/advanced-api/automatic-vendor-sharing/app1/src/Button.js b/advanced-api/automatic-vendor-sharing/app1/src/Button.js index 6abbdf0b186..24985b9752d 100644 --- a/advanced-api/automatic-vendor-sharing/app1/src/Button.js +++ b/advanced-api/automatic-vendor-sharing/app1/src/Button.js @@ -1,11 +1,69 @@ -import React from 'react'; +import React, { useState } from 'react'; -const style = { +const baseStyle = { background: '#800', color: '#fff', - padding: 12, + padding: '12px 20px', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + fontSize: '14px', + fontWeight: 'bold', + transition: 'all 0.2s ease', + display: 'flex', + alignItems: 'center', + gap: '8px' }; -const Button = () => ; +const Button = () => { + const [clickCount, setClickCount] = useState(0); + const [isHovered, setIsHovered] = useState(false); + + const handleClick = () => { + setClickCount(count => count + 1); + console.log('[App 1 Button] Clicked', clickCount + 1, 'times'); + }; + + const style = { + ...baseStyle, + background: isHovered ? '#a00' : '#800', + transform: isHovered ? 'translateY(-1px)' : 'translateY(0)', + boxShadow: isHovered ? '0 4px 8px rgba(0,0,0,0.2)' : '0 2px 4px rgba(0,0,0,0.1)' + }; + + return ( + + ); +}; export default Button; diff --git a/advanced-api/automatic-vendor-sharing/app1/src/ErrorBoundary.tsx b/advanced-api/automatic-vendor-sharing/app1/src/ErrorBoundary.tsx new file mode 100644 index 00000000000..7cd8a1cad78 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app1/src/ErrorBoundary.tsx @@ -0,0 +1,88 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: React.ComponentType<{ error: Error; retry: () => void }>; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Module Federation Error Boundary caught an error:', error, errorInfo); + } + + private retry = () => { + this.setState({ hasError: false, error: null }); + }; + + public render() { + if (this.state.hasError) { + const FallbackComponent = this.props.fallback; + + if (FallbackComponent) { + return ; + } + + return ( +
+

Module Loading Error

+

Failed to load remote module. This might be due to:

+
    +
  • Network connectivity issues
  • +
  • Remote application is not running
  • +
  • Version compatibility problems
  • +
+ +
+ Error Details +
+              {this.state.error?.message}
+              {this.state.error?.stack}
+            
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app1/src/bootstrap.js b/advanced-api/automatic-vendor-sharing/app1/src/bootstrap.js index a8680f71cdf..129ffb0c0f9 100644 --- a/advanced-api/automatic-vendor-sharing/app1/src/bootstrap.js +++ b/advanced-api/automatic-vendor-sharing/app1/src/bootstrap.js @@ -1,5 +1,7 @@ import App from './App'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -ReactDOM.render(, document.getElementById('root')); +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/advanced-api/automatic-vendor-sharing/app1/src/runtimePlugin.js b/advanced-api/automatic-vendor-sharing/app1/src/runtimePlugin.js new file mode 100644 index 00000000000..faab1c466f7 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app1/src/runtimePlugin.js @@ -0,0 +1,69 @@ +// Runtime plugin for Module Federation debugging and monitoring +const runtimePlugin = () => ({ + name: 'automatic-vendor-sharing-runtime', + + beforeInit(args) { + console.log('[MF Runtime] App1 - Initializing Module Federation with AutomaticVendorSharing'); + console.log('[MF Runtime] App1 - Available remotes:', Object.keys(args.remotes || {})); + return args; + }, + + beforeLoadShare(args) { + console.log('[MF Runtime] App1 - Loading shared dependency:', args.pkgName, 'version:', args.version); + + // Monitor shared dependency loading for optimization insights + if (args.pkgName === 'react' || args.pkgName === 'react-dom') { + console.log('[MF Runtime] App1 - Critical React dependency being shared'); + } + + return args; + }, + + beforeRequest(args) { + console.log('[MF Runtime] App1 - Requesting module:', args.id, 'from remote:', args.options?.remote); + + // Add performance monitoring + args.options = { + ...args.options, + metadata: { + ...args.options?.metadata, + requestTime: Date.now(), + source: 'app1' + } + }; + + return args; + }, + + afterResolve(args) { + if (args.options?.metadata?.requestTime) { + const loadTime = Date.now() - args.options.metadata.requestTime; + console.log(`[MF Runtime] App1 - Module ${args.id} loaded in ${loadTime}ms`); + } + return args; + }, + + errorLoadRemote(args) { + console.error('[MF Runtime] App1 - Failed to load remote module:', args); + + // Enhanced error reporting for debugging + const errorInfo = { + remote: args.id, + error: args.error?.message, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + url: window.location.href + }; + + console.error('[MF Runtime] App1 - Error details:', errorInfo); + + // In production, you might want to send this to an error tracking service + if (process.env.NODE_ENV === 'production') { + // Example: sendToErrorTracker(errorInfo); + } + + return args; + } +}); + +export default runtimePlugin; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app1/src/types/module-federation.d.ts b/advanced-api/automatic-vendor-sharing/app1/src/types/module-federation.d.ts new file mode 100644 index 00000000000..62d1d21ef54 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app1/src/types/module-federation.d.ts @@ -0,0 +1,11 @@ +declare module 'app2/Button' { + import { ComponentType } from 'react'; + const Button: ComponentType; + export default Button; +} + +declare module 'app2/ErrorBoundary' { + import { ComponentType } from 'react'; + const ErrorBoundary: ComponentType<{ children: React.ReactNode; fallback?: React.ComponentType }>; + export default ErrorBoundary; +} \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app1/tsconfig.json b/advanced-api/automatic-vendor-sharing/app1/tsconfig.json new file mode 100644 index 00000000000..ea1ad485ca3 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app1/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app1/webpack.config.js b/advanced-api/automatic-vendor-sharing/app1/webpack.config.js index 951976c7977..ab18b32f602 100644 --- a/advanced-api/automatic-vendor-sharing/app1/webpack.config.js +++ b/advanced-api/automatic-vendor-sharing/app1/webpack.config.js @@ -1,36 +1,18 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('@module-federation/enhanced'); -const path = require('path'); -// adds all your dependencies as shared modules -// version is inferred from package.json in the dependencies -// requiredVersion is used from your package.json -// dependencies will automatically use the highest available package -// in the federated app, based on version requirement in package.json -// multiple different versions might coexist in the federated app -// Note that this will not affect nested paths like "lodash/pluck" -// Note that this will disable some optimization on these packages -// with might lead the bundle size problems const deps = require('./package.json').dependencies; module.exports = { entry: './src/index', - cache: false, mode: 'development', devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, + port: 3001, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, - port: 3001, - }, - target: 'web', - output: { - publicPath: 'auto', }, module: { rules: [ @@ -55,12 +37,13 @@ module.exports = { './Button': './src/Button', }, shared: { - ...deps, react: { singleton: true, + requiredVersion: deps.react, }, 'react-dom': { singleton: true, + requiredVersion: deps['react-dom'], }, }, }), diff --git a/advanced-api/automatic-vendor-sharing/app1/webpack.prod.config.js b/advanced-api/automatic-vendor-sharing/app1/webpack.prod.config.js new file mode 100644 index 00000000000..4220e1d2021 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app1/webpack.prod.config.js @@ -0,0 +1,111 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { ModuleFederationPlugin, AutomaticVendorFederation } = require('@module-federation/enhanced'); + +// Production configuration for optimized builds +const deps = require('./package.json').dependencies; + +module.exports = { + entry: './src/index', + mode: 'production', + cache: { + type: 'filesystem', + buildDependencies: { + config: [__filename], + }, + }, + optimization: { + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10, + enforce: true, + }, + }, + }, + usedExports: true, + sideEffects: false, + }, + target: 'web', + output: { + publicPath: 'auto', + uniqueName: 'automatic_vendor_sharing_app1', + chunkLoadingGlobal: 'app1ChunkLoading', + clean: true, + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].chunk.js', + }, + module: { + rules: [ + { + test: /\.jsx?$/, + loader: 'babel-loader', + exclude: /node_modules/, + options: { + presets: [ + ['@babel/preset-react', { runtime: 'automatic' }], + ['@babel/preset-env', { + targets: { + browsers: ['> 1%', 'last 2 versions'] + }, + modules: false + }], + '@babel/preset-typescript' + ], + }, + }, + ], + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'app1', + filename: 'remoteEntry.js', + remotes: { + app2: 'app2@/remoteEntry.js', // Use relative URL for production + }, + exposes: { + './Button': './src/Button', + './ErrorBoundary': './src/ErrorBoundary', + }, + runtimePlugins: [require.resolve('./src/runtimePlugin')], + shared: { + ...AutomaticVendorFederation({ + exclude: ['@module-federation/enhanced'], + ignoreVersion: ['react', 'react-dom'], + shareStrategy: 'loaded-first', + eager: false, // Optimize for production loading + }), + react: { + singleton: true, + requiredVersion: deps.react, + eager: false, + }, + 'react-dom': { + singleton: true, + requiredVersion: deps['react-dom'], + eager: false, + }, + }, + experiments: { + federationRuntime: 'hoisted', + }, + }), + new HtmlWebpackPlugin({ + template: './public/index.html', + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, + }), + ], +}; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/ErrorBoundary.d.ts b/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/ErrorBoundary.d.ts new file mode 100644 index 00000000000..e95a476daf2 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/ErrorBoundary.d.ts @@ -0,0 +1,2 @@ +export * from './compiled-types/ErrorBoundary'; +export { default } from './compiled-types/ErrorBoundary'; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/apis.d.ts b/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/apis.d.ts new file mode 100644 index 00000000000..443736f43ea --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/apis.d.ts @@ -0,0 +1,3 @@ + + export type RemoteKeys = 'app1/Button' | 'app1/ErrorBoundary'; + type PackageType = T extends 'app1/ErrorBoundary' ? typeof import('app1/ErrorBoundary') :T extends 'app1/Button' ? typeof import('app1/Button') :any; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/compiled-types/ErrorBoundary.d.ts b/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/compiled-types/ErrorBoundary.d.ts new file mode 100644 index 00000000000..4077ec5246c --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/@mf-types/app1/compiled-types/ErrorBoundary.d.ts @@ -0,0 +1,20 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +interface Props { + children: ReactNode; + fallback?: React.ComponentType<{ + error: Error; + retry: () => void; + }>; +} +interface State { + hasError: boolean; + error: Error | null; +} +declare class ErrorBoundary extends Component { + state: State; + static getDerivedStateFromError(error: Error): State; + componentDidCatch(error: Error, errorInfo: ErrorInfo): void; + private retry; + render(): string | number | boolean | Iterable | import("react/jsx-runtime").JSX.Element | null | undefined; +} +export default ErrorBoundary; diff --git a/advanced-api/automatic-vendor-sharing/app2/@mf-types/index.d.ts b/advanced-api/automatic-vendor-sharing/app2/@mf-types/index.d.ts new file mode 100644 index 00000000000..6a745ccdf3f --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/@mf-types/index.d.ts @@ -0,0 +1,23 @@ +import type { PackageType as PackageType_0,RemoteKeys as RemoteKeys_0 } from './app1/apis.d.ts'; + declare module "@module-federation/runtime" { + type RemoteKeys = RemoteKeys_0; + type PackageType = T extends RemoteKeys_0 ? PackageType_0 : +Y ; + export function loadRemote(packageName: T): Promise>; + export function loadRemote(packageName: T): Promise>; + } +declare module "@module-federation/enhanced/runtime" { + type RemoteKeys = RemoteKeys_0; + type PackageType = T extends RemoteKeys_0 ? PackageType_0 : +Y ; + export function loadRemote(packageName: T): Promise>; + export function loadRemote(packageName: T): Promise>; + } +declare module "@module-federation/runtime-tools" { + type RemoteKeys = RemoteKeys_0; + type PackageType = T extends RemoteKeys_0 ? PackageType_0 : +Y ; + export function loadRemote(packageName: T): Promise>; + export function loadRemote(packageName: T): Promise>; + } + \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/package.json b/advanced-api/automatic-vendor-sharing/app2/package.json index a7891c71277..d713a487335 100644 --- a/advanced-api/automatic-vendor-sharing/app2/package.json +++ b/advanced-api/automatic-vendor-sharing/app2/package.json @@ -4,7 +4,11 @@ "devDependencies": { "@babel/core": "7.24.7", "@babel/preset-react": "7.24.7", - "@module-federation/enhanced": "0.17.1", + "@babel/preset-typescript": "^7.24.7", + "@module-federation/enhanced": "^0.17.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.5.4", "@rspack/cli": "1.4.11", "@rspack/core": "1.4.11", "@rspack/dev-server": "1.1.3", @@ -18,13 +22,16 @@ "scripts": { "start": "rspack serve -c rspack.config.js", "build": "rspack build --mode production -c rspack.config.js", + "build:prod": "webpack --config webpack.prod.config.js", "legacy:start": "webpack serve --config webpack.config.js", "legacy:build": "webpack --config webpack.config.js --mode production", "serve": "serve dist -p 3002", - "clean": "rm -rf dist" + "clean": "rm -rf dist", + "type-check": "tsc --noEmit", + "analyze": "webpack-bundle-analyzer dist/static/js/*.js" }, "dependencies": { - "react": "^16.13.0", - "react-dom": "^16.13.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" } } \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/rspack.config.js b/advanced-api/automatic-vendor-sharing/app2/rspack.config.js index 399141e22e0..99c0d3861ec 100644 --- a/advanced-api/automatic-vendor-sharing/app2/rspack.config.js +++ b/advanced-api/automatic-vendor-sharing/app2/rspack.config.js @@ -1,24 +1,17 @@ const { HtmlRspackPlugin, } = require('@rspack/core'); -const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack') +const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack'); const path = require('path'); - -// adds all your dependencies as shared modules -// version is inferred from package.json in the dependencies -// requiredVersion is used from your package.json -// dependencies will automatically use the highest available package -// in the federated app, based on version requirement in package.json -// multiple different versions might coexist in the federated app -// Note that this will not affect nested paths like "lodash/pluck" -// Note that this will disable some optimization on these packages -// with might lead the bundle size problems const deps = require('./package.json').dependencies; module.exports = { entry: './src/index', mode: 'development', + resolve: { + extensions: ['.tsx', '.ts', '.jsx', '.js'], + }, devServer: { static: { directory: path.join(__dirname, 'dist'), @@ -33,19 +26,20 @@ module.exports = { target: 'web', output: { publicPath: 'auto', + uniqueName: 'automatic_vendor_sharing_app2', }, module: { rules: [ { - test: /\.js$/, + test: /\.(js|jsx|ts|tsx)$/, include: path.resolve(__dirname, 'src'), use: { loader: 'builtin:swc-loader', options: { jsc: { parser: { - syntax: 'ecmascript', - jsx: true, + syntax: 'typescript', + tsx: true, }, transform: { react: { @@ -69,12 +63,13 @@ module.exports = { './Button': './src/Button', }, shared: { - ...deps, react: { singleton: true, + requiredVersion: deps.react, }, 'react-dom': { singleton: true, + requiredVersion: deps['react-dom'], }, }, }), @@ -82,4 +77,4 @@ module.exports = { template: './public/index.html', }), ], -}; +}; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/src/App.js b/advanced-api/automatic-vendor-sharing/app2/src/App.js index 19fc741b312..a76d2f4e9b7 100644 --- a/advanced-api/automatic-vendor-sharing/app2/src/App.js +++ b/advanced-api/automatic-vendor-sharing/app2/src/App.js @@ -1,17 +1,109 @@ import LocalButton from './Button'; -import React from 'react'; +import ErrorBoundary from './ErrorBoundary'; +import React, { Suspense, useState, useEffect } from 'react'; const RemoteButton = React.lazy(() => import('app1/Button')); -const App = () => ( -
-

Bi-Directional

-

App 2

- - - - +// Enhanced loading component with visual feedback +const LoadingFallback = ({ message = 'Loading remote module...' }) => ( +
+
+ {message} +
); +const App = () => { + const [remoteLoadTime, setRemoteLoadTime] = useState(null); + const [sharedDependencies, setSharedDependencies] = useState([]); + + useEffect(() => { + // Monitor shared dependencies for educational purposes + const startTime = Date.now(); + import('app1/Button').then(() => { + const loadTime = Date.now() - startTime; + setRemoteLoadTime(loadTime); + }).catch(console.error); + + // Simulate checking shared dependencies (in real app, this would come from Module Federation runtime) + setSharedDependencies(['react', 'react-dom']); + }, []); + + return ( +
+
+

Module Federation with Automatic Vendor Sharing

+

App 2 (Host & Remote)

+

+ Demonstrating intelligent dependency sharing across microfrontends +

+
+ +
+
+

Local Component

+ +

+ This button is served from App 2's local bundle +

+
+ +
+

Remote Component (App 1)

+ + }> + + + +

+ This button is loaded from App 1 via Module Federation + {remoteLoadTime && ` (loaded in ${remoteLoadTime}ms)`} +

+
+
+ +
+

Automatic Vendor Sharing Info

+

+ This example demonstrates AutomaticVendorFederation, which intelligently shares dependencies + between microfrontends to optimize bundle sizes and prevent duplicate code. +

+
+ Shared Dependencies: {sharedDependencies.join(', ')} +
+ Load Strategy: loaded-first (uses the first loaded version) +
+ Benefits: Reduced bundle size, faster loading, consistent dependency versions +
+
+
+ ); +}; + export default App; diff --git a/advanced-api/automatic-vendor-sharing/app2/src/Button.js b/advanced-api/automatic-vendor-sharing/app2/src/Button.js index 1a130e73049..45fd1ead9d9 100644 --- a/advanced-api/automatic-vendor-sharing/app2/src/Button.js +++ b/advanced-api/automatic-vendor-sharing/app2/src/Button.js @@ -1,11 +1,69 @@ -import React from 'react'; +import React, { useState } from 'react'; -const style = { +const baseStyle = { background: '#00c', color: '#fff', - padding: 12, + padding: '12px 20px', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + fontSize: '14px', + fontWeight: 'bold', + transition: 'all 0.2s ease', + display: 'flex', + alignItems: 'center', + gap: '8px' }; -const Button = () => ; +const Button = () => { + const [clickCount, setClickCount] = useState(0); + const [isHovered, setIsHovered] = useState(false); + + const handleClick = () => { + setClickCount(count => count + 1); + console.log('[App 2 Button] Clicked', clickCount + 1, 'times'); + }; + + const style = { + ...baseStyle, + background: isHovered ? '#00e' : '#00c', + transform: isHovered ? 'translateY(-1px)' : 'translateY(0)', + boxShadow: isHovered ? '0 4px 8px rgba(0,0,0,0.2)' : '0 2px 4px rgba(0,0,0,0.1)' + }; + + return ( + + ); +}; export default Button; diff --git a/advanced-api/automatic-vendor-sharing/app2/src/ErrorBoundary.tsx b/advanced-api/automatic-vendor-sharing/app2/src/ErrorBoundary.tsx new file mode 100644 index 00000000000..7cd8a1cad78 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/src/ErrorBoundary.tsx @@ -0,0 +1,88 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: React.ComponentType<{ error: Error; retry: () => void }>; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Module Federation Error Boundary caught an error:', error, errorInfo); + } + + private retry = () => { + this.setState({ hasError: false, error: null }); + }; + + public render() { + if (this.state.hasError) { + const FallbackComponent = this.props.fallback; + + if (FallbackComponent) { + return ; + } + + return ( +
+

Module Loading Error

+

Failed to load remote module. This might be due to:

+
    +
  • Network connectivity issues
  • +
  • Remote application is not running
  • +
  • Version compatibility problems
  • +
+ +
+ Error Details +
+              {this.state.error?.message}
+              {this.state.error?.stack}
+            
+
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/src/bootstrap.js b/advanced-api/automatic-vendor-sharing/app2/src/bootstrap.js index a8680f71cdf..129ffb0c0f9 100644 --- a/advanced-api/automatic-vendor-sharing/app2/src/bootstrap.js +++ b/advanced-api/automatic-vendor-sharing/app2/src/bootstrap.js @@ -1,5 +1,7 @@ import App from './App'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -ReactDOM.render(, document.getElementById('root')); +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/advanced-api/automatic-vendor-sharing/app2/src/runtimePlugin.js b/advanced-api/automatic-vendor-sharing/app2/src/runtimePlugin.js new file mode 100644 index 00000000000..3898b3a6b4c --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/src/runtimePlugin.js @@ -0,0 +1,69 @@ +// Runtime plugin for Module Federation debugging and monitoring +const runtimePlugin = () => ({ + name: 'automatic-vendor-sharing-runtime', + + beforeInit(args) { + console.log('[MF Runtime] App2 - Initializing Module Federation with AutomaticVendorSharing'); + console.log('[MF Runtime] App2 - Available remotes:', Object.keys(args.remotes || {})); + return args; + }, + + beforeLoadShare(args) { + console.log('[MF Runtime] App2 - Loading shared dependency:', args.pkgName, 'version:', args.version); + + // Monitor shared dependency loading for optimization insights + if (args.pkgName === 'react' || args.pkgName === 'react-dom') { + console.log('[MF Runtime] App2 - Critical React dependency being shared'); + } + + return args; + }, + + beforeRequest(args) { + console.log('[MF Runtime] App2 - Requesting module:', args.id, 'from remote:', args.options?.remote); + + // Add performance monitoring + args.options = { + ...args.options, + metadata: { + ...args.options?.metadata, + requestTime: Date.now(), + source: 'app2' + } + }; + + return args; + }, + + afterResolve(args) { + if (args.options?.metadata?.requestTime) { + const loadTime = Date.now() - args.options.metadata.requestTime; + console.log(`[MF Runtime] App2 - Module ${args.id} loaded in ${loadTime}ms`); + } + return args; + }, + + errorLoadRemote(args) { + console.error('[MF Runtime] App2 - Failed to load remote module:', args); + + // Enhanced error reporting for debugging + const errorInfo = { + remote: args.id, + error: args.error?.message, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + url: window.location.href + }; + + console.error('[MF Runtime] App2 - Error details:', errorInfo); + + // In production, you might want to send this to an error tracking service + if (process.env.NODE_ENV === 'production') { + // Example: sendToErrorTracker(errorInfo); + } + + return args; + } +}); + +export default runtimePlugin; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/src/types/module-federation.d.ts b/advanced-api/automatic-vendor-sharing/app2/src/types/module-federation.d.ts new file mode 100644 index 00000000000..c285830e678 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/src/types/module-federation.d.ts @@ -0,0 +1,11 @@ +declare module 'app1/Button' { + import { ComponentType } from 'react'; + const Button: ComponentType; + export default Button; +} + +declare module 'app1/ErrorBoundary' { + import { ComponentType } from 'react'; + const ErrorBoundary: ComponentType<{ children: React.ReactNode; fallback?: React.ComponentType }>; + export default ErrorBoundary; +} \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/tsconfig.json b/advanced-api/automatic-vendor-sharing/app2/tsconfig.json new file mode 100644 index 00000000000..ea1ad485ca3 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/app2/webpack.config.js b/advanced-api/automatic-vendor-sharing/app2/webpack.config.js index bddb0e301b2..2dbb7ad2f94 100644 --- a/advanced-api/automatic-vendor-sharing/app2/webpack.config.js +++ b/advanced-api/automatic-vendor-sharing/app2/webpack.config.js @@ -1,36 +1,18 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('@module-federation/enhanced'); -const path = require('path'); -// adds all your dependencies as shared modules -// version is inferred from package.json in the dependencies -// requiredVersion is used from your package.json -// dependencies will automatically use the highest available package -// in the federated app, based on version requirement in package.json -// multiple different versions might coexist in the federated app -// Note that this will not affect nested paths like "lodash/pluck" -// Note that this will disable some optimization on these packages -// with might lead the bundle size problems const deps = require('./package.json').dependencies; module.exports = { entry: './src/index', mode: 'development', - cache: false, devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, + port: 3002, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, - port: 3002, - }, - target: 'web', - output: { - publicPath: 'auto', }, module: { rules: [ @@ -54,13 +36,15 @@ module.exports = { exposes: { './Button': './src/Button', }, + runtimePlugins: [require.resolve('./src/runtimePlugin')], shared: { - ...deps, react: { singleton: true, + requiredVersion: deps.react, }, 'react-dom': { singleton: true, + requiredVersion: deps['react-dom'], }, }, }), diff --git a/advanced-api/automatic-vendor-sharing/app2/webpack.prod.config.js b/advanced-api/automatic-vendor-sharing/app2/webpack.prod.config.js new file mode 100644 index 00000000000..b10f4ea7bb3 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/app2/webpack.prod.config.js @@ -0,0 +1,111 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { ModuleFederationPlugin, AutomaticVendorFederation } = require('@module-federation/enhanced'); + +// Production configuration for optimized builds +const deps = require('./package.json').dependencies; + +module.exports = { + entry: './src/index', + mode: 'production', + cache: { + type: 'filesystem', + buildDependencies: { + config: [__filename], + }, + }, + optimization: { + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10, + enforce: true, + }, + }, + }, + usedExports: true, + sideEffects: false, + }, + target: 'web', + output: { + publicPath: 'auto', + uniqueName: 'automatic_vendor_sharing_app2', + chunkLoadingGlobal: 'app2ChunkLoading', + clean: true, + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].chunk.js', + }, + module: { + rules: [ + { + test: /\.jsx?$/, + loader: 'babel-loader', + exclude: /node_modules/, + options: { + presets: [ + ['@babel/preset-react', { runtime: 'automatic' }], + ['@babel/preset-env', { + targets: { + browsers: ['> 1%', 'last 2 versions'] + }, + modules: false + }], + '@babel/preset-typescript' + ], + }, + }, + ], + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'app2', + filename: 'remoteEntry.js', + remotes: { + app1: 'app1@/remoteEntry.js', // Use relative URL for production + }, + exposes: { + './Button': './src/Button', + './ErrorBoundary': './src/ErrorBoundary', + }, + runtimePlugins: [require.resolve('./src/runtimePlugin')], + shared: { + ...AutomaticVendorFederation({ + exclude: ['@module-federation/enhanced'], + ignoreVersion: ['react', 'react-dom'], + shareStrategy: 'loaded-first', + eager: false, // Optimize for production loading + }), + react: { + singleton: true, + requiredVersion: deps.react, + eager: false, + }, + 'react-dom': { + singleton: true, + requiredVersion: deps['react-dom'], + eager: false, + }, + }, + experiments: { + federationRuntime: 'hoisted', + }, + }), + new HtmlWebpackPlugin({ + template: './public/index.html', + minify: { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, + }), + ], +}; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts b/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts new file mode 100644 index 00000000000..6bd6ba97b75 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/e2e/checkAutomaticVendorApps.spec.ts @@ -0,0 +1,176 @@ +import { test, expect, Page } from '@playwright/test'; + +// Helper functions +async function openLocalhost(page: Page, port: number) { + await page.goto(`http://localhost:${port}`); + await page.waitForLoadState('networkidle'); +} + +async function checkElementWithTextPresence(page: Page, selector: string, text: string) { + const element = page.locator(`${selector}:has-text("${text}")`); + await expect(element).toBeVisible(); +} + +async function clickElementWithText(page: Page, selector: string, text: string) { + await page.click(`${selector}:has-text("${text}")`); +} + + + +const appsData = [ + { + headerText: 'Module Federation with Automatic Vendor Sharing', + appNameText: 'App 1 (Host & Remote)', + buttonColor: 'rgb(255, 0, 0)', + host: 3001, + }, + { + headerText: 'Module Federation with Automatic Vendor Sharing', + appNameText: 'App 2 (Host & Remote)', + buttonColor: 'rgb(0, 0, 139)', + host: 3002, + }, +]; + +test.describe('Automatic Vendor Sharing E2E Tests', () => { + + appsData.forEach((appData) => { + const { host, appNameText, headerText } = appData; + + test.describe(`Check ${appNameText}`, () => { + test(`should display ${appNameText} header and subheader correctly`, async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + await openLocalhost(page, host); + + // Check header and subheader exist + await checkElementWithTextPresence(page, 'h1', headerText); + await checkElementWithTextPresence(page, 'h2', appNameText); + + // Verify no critical console errors + const criticalErrors = consoleErrors.filter(error => + error.includes('Failed to fetch') || + error.includes('ChunkLoadError') || + error.includes('Module not found') || + error.includes('TypeError') + ); + expect(criticalErrors).toHaveLength(0); + }); + + test(`should display ${appNameText} button correctly`, async ({ page }) => { + await openLocalhost(page, host); + + const buttonText = `${appNameText.split(' ')[0]} ${appNameText.split(' ')[1]} Button`; + + // Check button exists with correct text + await checkElementWithTextPresence(page, 'button', buttonText); + }); + + test(`should handle ${appNameText} button interactions`, async ({ page }) => { + await openLocalhost(page, host); + + const buttonText = `${appNameText.split(' ')[0]} ${appNameText.split(' ')[1]} Button`; + + // Click the button and verify it responds + await clickElementWithText(page, 'button', buttonText); + + // Verify button is still visible and functional after click + await checkElementWithTextPresence(page, 'button', buttonText); + }); + }); + }); + + test.describe('Cross-App Integration Tests', () => { + test('should demonstrate automatic vendor sharing between apps', async ({ page }) => { + const networkRequests: string[] = []; + + page.on('request', (request) => { + networkRequests.push(request.url()); + }); + + // Visit both apps to trigger vendor sharing + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + await page.goto('http://localhost:3002'); + await page.waitForLoadState('networkidle'); + + // Verify shared dependencies are loaded efficiently + const reactRequests = networkRequests.filter(url => + url.includes('react') && !url.includes('react-dom') + ); + + // Should not load React multiple times due to vendor sharing + expect(reactRequests.length).toBeLessThanOrEqual(10); + }); + + test('should handle CORS correctly for federated modules', async ({ page }) => { + const corsErrors: string[] = []; + page.on('response', (response) => { + if (response.status() >= 400 && response.url().includes('localhost:300')) { + corsErrors.push(`${response.status()} - ${response.url()}`); + } + }); + + // Test cross-origin requests work properly + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Should have no CORS errors + expect(corsErrors).toHaveLength(0); + }); + + test('should load applications within reasonable time', async ({ page }) => { + const startTime = Date.now(); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + const loadTime = Date.now() - startTime; + expect(loadTime).toBeLessThan(10000); // Should load within 10 seconds + }); + }); + + test.describe('AutomaticVendorFederation Features', () => { + test('should demonstrate shared vendor optimization', async ({ page }) => { + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Check that the main elements are present + await checkElementWithTextPresence(page, 'h1', 'Module Federation with Automatic Vendor Sharing'); + await checkElementWithTextPresence(page, 'h2', 'App 1 (Host & Remote)'); + }); + + test('should handle error boundaries correctly', async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Click button to test functionality + const buttonExists = await page.locator('button').first().isVisible(); + if (buttonExists) { + await page.locator('button').first().click(); + await page.waitForTimeout(1000); + } + + // Should handle any errors gracefully + const criticalErrors = consoleErrors.filter(error => + error.includes('Uncaught') && + !error.includes('webpack-dev-server') && + !error.includes('DevTools') + ); + expect(criticalErrors).toHaveLength(0); + }); + }); +}); \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/e2e/utils/base-test.ts b/advanced-api/automatic-vendor-sharing/e2e/utils/base-test.ts new file mode 100644 index 00000000000..858fd3b49f5 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/e2e/utils/base-test.ts @@ -0,0 +1,46 @@ +import { Page } from '@playwright/test'; + +export class BasePage { + constructor(public page: Page) {} + + async openLocalhost(port: number) { + await this.page.goto(`http://localhost:${port}`); + await this.page.waitForLoadState('networkidle'); + } + + async checkElementWithTextPresence(selector: string, text: string, timeout: number = 10000) { + await this.page.locator(selector).filter({ hasText: text }).waitFor({ timeout }); + } + + async checkElementVisibility(selector: string, timeout: number = 10000) { + await this.page.locator(selector).waitFor({ state: 'visible', timeout }); + } + + async checkElementBackgroundColor(selector: string, expectedColor: string) { + const element = this.page.locator(selector); + await element.waitFor({ state: 'visible' }); + const backgroundColor = await element.evaluate((el) => { + return window.getComputedStyle(el).backgroundColor; + }); + if (backgroundColor !== expectedColor) { + throw new Error(`Expected background color ${expectedColor}, but got ${backgroundColor}`); + } + } + + async clickElementWithText(selector: string, text: string) { + await this.page.locator(selector).filter({ hasText: text }).click(); + } + + async waitForDynamicImport(timeout: number = 5000) { + // Wait for any dynamic imports to complete + await this.page.waitForTimeout(1000); + await this.page.waitForLoadState('networkidle', { timeout }); + } + + async checkDateFormat() { + // Check for moment.js formatted date + const dateElement = this.page.locator('text=/\\d{1,2}\\/\\d{1,2}\\/\\d{4}/'); + await dateElement.waitFor({ timeout: 5000 }); + } +} + diff --git a/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts b/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts new file mode 100644 index 00000000000..96b1b49f2ed --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/e2e/utils/constants.ts @@ -0,0 +1,14 @@ +export const Constants = { + commonConstantsData: { + biDirectional: 'Module Federation with Automatic Vendor Sharing', + button: 'Button', + commonCountAppNames: { + app1: 'App 1 (Host & Remote)', + app2: 'App 2 (Host & Remote)', + }, + }, + color: { + red: 'rgb(255, 0, 0)', + deepBlue: 'rgb(0, 0, 139)', + }, +}; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts b/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts new file mode 100644 index 00000000000..6761574198a --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/e2e/utils/selectors.ts @@ -0,0 +1,15 @@ +export const selectors = { + dataTestIds: { + app1Button: '[data-e2e="APP_1__BUTTON"]', + app2Button: '[data-e2e="APP_2__BUTTON"]', + }, + tags: { + headers: { + h1: 'h1', + h2: 'h2', + }, + coreElements: { + button: 'button', + }, + }, +}; \ No newline at end of file diff --git a/advanced-api/automatic-vendor-sharing/package.json b/advanced-api/automatic-vendor-sharing/package.json index 41f70586f0b..bea46fd6151 100644 --- a/advanced-api/automatic-vendor-sharing/package.json +++ b/advanced-api/automatic-vendor-sharing/package.json @@ -14,10 +14,11 @@ "clean": "pnpm --filter automatic-vendor-sharing_app* --parallel clean", "legacy:start": "pnpm --filter automatic-vendor-sharing_app* --parallel legacy:start", "legacy:build": "pnpm --filter automatic-vendor-sharing_app* --parallel legacy:build", - "e2e:ci": "pnpm start & wait-on http-get://localhost:3001/ && npx cypress run --config-file ../../cypress-e2e/config/cypress.config.ts --config '{\"supportFile\": \"../../cypress-e2e/support/e2e.ts\"}' --spec \"./e2e/*.cy.ts\" --browser=chrome", - "legacy:e2e:ci": "pnpm legacy:start & wait-on http-get://localhost:3001/ && npx cypress run --config-file ../../cypress-e2e/config/cypress.config.ts --config '{\"supportFile\": \"../../cypress-e2e/support/e2e.ts\"}' --spec \"./e2e/*.cy.ts\" --browser=chrome" + "e2e:ci": "npx playwright test", + "legacy:e2e:ci": "npx playwright test" }, "devDependencies": { + "@playwright/test": "^1.54.2", "wait-on": "7.2.0" } } diff --git a/advanced-api/automatic-vendor-sharing/playwright.config.ts b/advanced-api/automatic-vendor-sharing/playwright.config.ts new file mode 100644 index 00000000000..a8727741bd5 --- /dev/null +++ b/advanced-api/automatic-vendor-sharing/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 60000, + expect: { + timeout: 15000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['list'], + ], + use: { + baseURL: 'http://localhost:3001', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + viewport: { width: 1920, height: 1080 }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: [ + { + command: 'pnpm --filter automatic-vendor-sharing_app1 start', + port: 3001, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + { + command: 'pnpm --filter automatic-vendor-sharing_app2 start', + port: 3002, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + ], +}); \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/README.md b/advanced-api/dynamic-remotes-runtime-environment-variables/README.md index 85055f66ef5..64838b454e2 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/README.md +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/README.md @@ -1,24 +1,594 @@ -# Module Federation Dynamic Remotes With Runtime Environment Variables +# Module Federation Dynamic Remotes with Runtime Environment Variables -This example shows how to implement runtime environment variables when using dynamic remotes in module federation. +A comprehensive example demonstrating how to implement **runtime environment variables** in Module Federation applications with containerized deployment. This example showcases modern micro-frontend architecture with dynamic remote loading, enhanced error handling, and production-ready Docker configurations. -- `host` is the host application. -- `remote` standalone application which exposes `Widget` component. +## 📋 Table of Contents -The runtime environment variables when using Client-Side-Rendering solution was inspired by this article: -[https://www.freecodecamp.org/news/how-to-implement-runtime-environment-variables-with-create-react-app-docker-and-nginx-7f9d42a91d70/] +- [Overview](#overview) +- [Architecture](#architecture) +- [Key Features](#key-features) +- [Prerequisites](#prerequisites) +- [Quick Start](#quick-start) +- [Development Setup](#development-setup) +- [Docker Deployment](#docker-deployment) +- [Environment Configuration](#environment-configuration) +- [Production Deployment](#production-deployment) +- [Monitoring & Health Checks](#monitoring--health-checks) +- [Security Considerations](#security-considerations) +- [Troubleshooting](#troubleshooting) +- [Best Practices](#best-practices) +- [API Reference](#api-reference) -# Running Demo +## 🎯 Overview -Run `pnpm start`. This will build and serve both `host` and `remote` on ports 3000 and 3001 respectively. +This example demonstrates the power of **runtime environment variables** in micro-frontend architectures. Unlike traditional build-time configuration, runtime configuration allows you to: -- [localhost:3000](http://localhost:3000/) (HOST) -- [localhost:3001](http://localhost:3001/) (STANDALONE REMOTE) +- Deploy the same container image across different environments (dev, staging, production) +- Change configuration without rebuilding applications +- Implement environment-specific remote URLs and API endpoints +- Enable true CI/CD with immutable artifacts -# Running Cypress E2E Tests +### What are Runtime Environment Variables? -To run tests in interactive mode, run `npm run cypress:debug` from the root directory of the project. It will open Cypress Test Runner and allow to run tests in interactive mode. [More info about "How to run tests"](../../cypress-e2e/README.md#how-to-run-tests) +Runtime environment variables are configuration values that are: +- **Injected at container startup** rather than build time +- **Environment-specific** without requiring separate builds +- **Dynamically loaded** by the client application +- **Cached appropriately** to balance performance and freshness -To build app and run test in headless mode, run `yarn e2e:ci`. It will build app and run tests for this workspace in headless mode. If tets failed cypress will create `cypress` directory in sample root folder with screenshots and videos. +## 🏗️ Architecture -["Best Practices, Rules amd more interesting information here](../../cypress-e2e/README.md) +``` +┌─────────────────┐ HTTP/HTTPS ┌─────────────────┐ +│ Host App │ ────────────────► │ Remote App │ +│ (Port 3000) │ Module Fed. │ (Port 3001) │ +└─────────────────┘ └─────────────────┘ + │ │ + ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ +│ env-config.json│ │ env-config.json│ +│ Runtime Config │ │ Runtime Config │ +└─────────────────┘ └─────────────────┘ +``` + +### Applications + +- **Host Application**: Main micro-frontend that dynamically loads remotes +- **Remote Application**: Standalone micro-frontend exposing a Widget component + +Both applications support: +- ✅ React 18 with concurrent features +- ✅ Enhanced error boundaries and retry mechanisms +- ✅ Runtime environment configuration +- ✅ Docker containerization with multi-stage builds +- ✅ Health checks and monitoring +- ✅ Security hardened nginx configurations + +## ✨ Key Features + +### 🚀 Modern Module Federation +- **@module-federation/enhanced** with latest patterns +- **Dynamic remote loading** with runtime URLs +- **Advanced error handling** with retry mechanisms +- **Component caching** with TTL (Time To Live) +- **Preloading capabilities** for performance optimization + +### 🔧 Environment Management +- **Runtime configuration** via `env-config.json` +- **Environment variable validation** with fallbacks +- **Multi-environment support** (dev, staging, prod) +- **Hot configuration reloading** (development) + +### 🐳 Production-Ready Containers +- **Multi-stage Docker builds** for optimization +- **Security hardening** with non-root users +- **Health checks** for container orchestration +- **Nginx optimizations** with caching and compression + +### 🛡️ Security & Performance +- **Content Security Policy** headers +- **CORS configuration** for cross-origin requests +- **Gzip compression** for static assets +- **Asset caching strategies** for optimal performance + +## 📋 Prerequisites + +- **Node.js** 18+ and npm/yarn/pnpm +- **Docker** (for containerized deployment) +- **Docker Compose** (optional, for orchestration) + +## 🚀 Quick Start + +```bash +# Clone the repository +git clone +cd dynamic-remotes-runtime-environment-variables + +# Install dependencies +pnpm install + +# Start both applications +pnpm start + +# Applications will be available at: +# Host: http://localhost:3000 +# Remote: http://localhost:3001 +``` + +## 💻 Development Setup + +### 1. Install Dependencies +```bash +# Install root dependencies +pnpm install + +# Install dependencies for both apps +pnpm --filter dynamic-remotes-runtime-environment-variables_* install +``` + +### 2. Environment Configuration +Create `.env` files in both `host/` and `remote/` directories: + +**host/.env** +```bash +API_URL=https://host.api.com +REMOTE_URL=http://localhost:3001/remoteEntry.js +NODE_ENV=development +``` + +**remote/.env** +```bash +API_URL=https://remote.api.com +NODE_ENV=development +``` + +### 3. Development Commands + +```bash +# Start both applications (Rspack - recommended) +pnpm start + +# Start with Webpack (legacy) +pnpm legacy:start + +# Build for production +pnpm build + +# Clean build artifacts +pnpm clean + +# Run tests +pnpm e2e:ci +``` + +## 🐳 Docker Deployment + +### Building Images + +```bash +# Build both Docker images +pnpm docker:build + +# Or build individually +cd host && docker build -t mf-host:latest . +cd remote && docker build -t mf-remote:latest . +``` + +### Running Containers + +```bash +# Run with runtime environment variables +docker run -d \ + --name mf-host \ + -p 3000:80 \ + -e API_URL=https://prod-host.api.com \ + -e REMOTE_URL=https://prod-remote.example.com/remoteEntry.js \ + mf-host:latest + +docker run -d \ + --name mf-remote \ + -p 3001:80 \ + -e API_URL=https://prod-remote.api.com \ + mf-remote:latest +``` + +### Docker Compose Example + +```yaml +version: '3.8' +services: + host: + build: ./host + ports: + - "3000:80" + environment: + - API_URL=https://host.api.com + - REMOTE_URL=http://remote/remoteEntry.js + depends_on: + - remote + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + + remote: + build: ./remote + ports: + - "3001:80" + environment: + - API_URL=https://remote.api.com + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 +``` + +## ⚙️ Environment Configuration + +### Configuration Flow + +1. **Build Time**: Default values in `env-config.json` +2. **Container Start**: `env.sh` reads environment variables +3. **Runtime**: Applications fetch `/env-config.json` +4. **Hot Reload**: Configuration updates without restart (development) + +### Environment Variables + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `API_URL` | Backend API endpoint | `https://host.api.com` | Yes | +| `REMOTE_URL` | Remote entry point URL | `http://localhost:3001/remoteEntry.js` | Host only | +| `NODE_ENV` | Environment mode | `development` | No | +| `VERSION` | Application version | `1.0.0` | No | + +### Advanced Configuration + +```javascript +// Custom validation in useFetchJson +const { data, loading, error } = useFetchJson('/env-config.json', { + validateData: (config) => { + return config.API_URL && config.API_URL.startsWith('https://'); + }, + fallbackData: { + API_URL: 'https://fallback.api.com', + REMOTE_URL: 'http://localhost:3001/remoteEntry.js' + }, + maxRetries: 3, + timeout: 5000 +}); +``` + +## 🏭 Production Deployment + +### Kubernetes Deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mf-host +spec: + replicas: 3 + selector: + matchLabels: + app: mf-host + template: + metadata: + labels: + app: mf-host + spec: + containers: + - name: mf-host + image: mf-host:latest + ports: + - containerPort: 80 + env: + - name: API_URL + value: "https://prod-api.example.com" + - name: REMOTE_URL + value: "https://remote.example.com/remoteEntry.js" + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" +``` + +### CDN and Load Balancer Configuration + +For production deployments, consider: + +1. **CDN Distribution**: Serve static assets from CDN +2. **Load Balancing**: Distribute traffic across multiple instances +3. **SSL Termination**: Handle HTTPS at load balancer level +4. **Health Check Integration**: Use `/health` endpoint for monitoring + +## 📊 Monitoring & Health Checks + +### Health Check Endpoints + +Both applications expose health check endpoints: + +```bash +# Check application health +curl http://localhost:3000/health # Host app +curl http://localhost:3001/health # Remote app + +# Check configuration +curl http://localhost:3000/env-config.json +curl http://localhost:3001/env-config.json +``` + +### Container Health Checks + +Docker containers include built-in health checks: + +```dockerfile +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 +``` + +### Monitoring Integration + +The applications log performance metrics and errors: + +```javascript +// Performance monitoring +console.log('Host App Performance:', { + loadTime: Math.round(perfData.loadEventEnd - perfData.loadEventStart), + domContentLoaded: Math.round(perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart), + totalTime: Math.round(perfData.loadEventEnd - perfData.fetchStart) +}); +``` + +## 🛡️ Security Considerations + +### Security Headers + +Both applications include comprehensive security headers: + +```nginx +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-XSS-Protection "1; mode=block" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3001; ..." always; +``` + +### Container Security + +- **Non-root user**: Containers run as unprivileged user +- **Multi-stage builds**: Minimize attack surface +- **Security updates**: Base images kept current +- **File permissions**: Restricted access to sensitive files + +### CORS Configuration + +Development uses permissive CORS. For production: + +```nginx +# Restrict CORS to specific origins +add_header 'Access-Control-Allow-Origin' 'https://trusted-domain.com' always; +``` + +## 🔧 Troubleshooting + +### Common Issues + +#### 1. Remote Loading Failures + +**Symptoms**: "Failed to load remote" errors in console + +**Solutions**: +```javascript +// Check remote URL accessibility +curl http://localhost:3001/remoteEntry.js + +// Verify CORS configuration +// Check network connectivity +// Validate environment variables +``` + +#### 2. Configuration Loading Issues + +**Symptoms**: Application shows fallback values + +**Solutions**: +```bash +# Verify env-config.json is accessible +curl http://localhost:3000/env-config.json + +# Check environment variable injection +docker exec -it container-name cat /usr/share/nginx/html/env-config.json + +# Validate JSON syntax +cat env-config.json | jq . +``` + +#### 3. Build Failures + +**Symptoms**: Docker build or npm build errors + +**Solutions**: +```bash +# Clear node modules and reinstall +rm -rf node_modules package-lock.json +npm install + +# Clear Docker build cache +docker system prune -a + +# Check Node.js version compatibility +node --version # Should be 18+ +``` + +### Debug Mode + +Enable debug logging: + +```bash +# Development +DEBUG=mf:* npm start + +# Docker +docker run -e DEBUG=mf:* mf-host:latest +``` + +### Performance Issues + +Monitor bundle sizes and loading times: + +```bash +# Analyze bundle +npx webpack-bundle-analyzer dist/ + +# Check loading performance +curl -w "@curl-format.txt" -o /dev/null -s http://localhost:3000/ +``` + +## 📚 Best Practices + +### 1. Environment Configuration +- ✅ Use runtime configuration for multi-environment deployments +- ✅ Implement proper fallback values +- ✅ Validate configuration at startup +- ✅ Never cache configuration responses + +### 2. Error Handling +- ✅ Implement retry mechanisms for remote loading +- ✅ Use error boundaries for graceful degradation +- ✅ Provide meaningful error messages to users +- ✅ Log errors for monitoring and debugging + +### 3. Performance Optimization +- ✅ Implement component caching with appropriate TTL +- ✅ Use preloading for critical remotes +- ✅ Optimize bundle splitting and chunk loading +- ✅ Implement proper asset caching strategies + +### 4. Security +- ✅ Use HTTPS in production +- ✅ Implement proper CORS policies +- ✅ Apply security headers +- ✅ Run containers as non-root users + +### 5. Monitoring +- ✅ Implement health checks +- ✅ Monitor remote loading success rates +- ✅ Track performance metrics +- ✅ Set up alerting for failures + +## 🔌 API Reference + +### useFederatedComponent Hook + +```typescript +const { + Component, + loading, + error, + retry, + retryCount, + clearCache +} = useFederatedComponent(remoteUrl, scope, module, options); +``` + +**Parameters**: +- `remoteUrl`: URL to remote entry point +- `scope`: Remote application scope name +- `module`: Exposed module path +- `options`: Configuration object + +**Options**: +```typescript +{ + maxRetries?: number; // Default: 3 + retryDelay?: number; // Default: 1000ms + enableCache?: boolean; // Default: true + timeout?: number; // Default: 10000ms +} +``` + +### useFetchJson Hook + +```typescript +const { + data, + loading, + error, + retry, + retryCount +} = useFetchJson(path, options); +``` + +**Parameters**: +- `path`: URL to JSON resource +- `options`: Configuration object + +**Options**: +```typescript +{ + maxRetries?: number; // Default: 3 + retryDelay?: number; // Default: 1000ms + timeout?: number; // Default: 5000ms + validateData?: (data: any) => boolean; // Data validator + fallbackData?: any; // Fallback on failure +} +``` + +### preloadRemote Function + +```typescript +await preloadRemote(remoteUrl, scope, module, options); +``` + +Preloads a remote component for improved performance. + +## 📝 Changelog + +### v2.0.0 (Enhanced) +- ✅ Upgraded to React 18 with concurrent features +- ✅ Enhanced error handling and retry mechanisms +- ✅ Improved Docker configurations with security hardening +- ✅ Added comprehensive monitoring and health checks +- ✅ Modernized webpack configurations +- ✅ Added comprehensive documentation + +### v1.0.0 (Original) +- ✅ Basic Module Federation setup +- ✅ Runtime environment variables +- ✅ Docker containerization +- ✅ CORS configuration + +## 🤝 Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Update documentation +6. Submit a pull request + +## 📄 License + +This project is part of the Module Federation examples repository. + +--- + +**Need help?** Check the [troubleshooting section](#troubleshooting) or open an issue in the repository. \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/docker-compose.yml b/advanced-api/dynamic-remotes-runtime-environment-variables/docker-compose.yml new file mode 100644 index 00000000000..9af8a3d344f --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/docker-compose.yml @@ -0,0 +1,65 @@ +version: '3.8' + +services: + host: + build: + context: ./host + dockerfile: Dockerfile + ports: + - "3000:80" + environment: + - API_URL=https://host.api.com + - REMOTE_URL=http://remote/remoteEntry.js + - NODE_ENV=production + - VERSION=2.0.0 + depends_on: + - remote + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + restart: unless-stopped + networks: + - mf-network + + remote: + build: + context: ./remote + dockerfile: Dockerfile + ports: + - "3001:80" + environment: + - API_URL=https://remote.api.com + - NODE_ENV=production + - VERSION=2.0.0 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + restart: unless-stopped + networks: + - mf-network + +networks: + mf-network: + driver: bridge + +# For development with custom environment variables +# Copy this file to docker-compose.override.yml and modify as needed +# +# Example docker-compose.override.yml: +# version: '3.8' +# services: +# host: +# environment: +# - API_URL=https://dev-host.api.com +# - REMOTE_URL=http://localhost:3001/remoteEntry.js +# - NODE_ENV=development +# remote: +# environment: +# - API_URL=https://dev-remote.api.com +# - NODE_ENV=development \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts new file mode 100644 index 00000000000..3b4d43414c9 --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/checkDynamicRemotesRuntimesApps.spec.ts @@ -0,0 +1,185 @@ +import { test, expect, Page } from '@playwright/test'; + +// Helper functions +async function openLocalhost(page: Page, port: number) { + // Set up console and error logging + const consoleMessages: string[] = []; + const pageErrors: string[] = []; + + page.on('console', (msg) => { + consoleMessages.push(`[${msg.type()}] ${msg.text()}`); + }); + + page.on('pageerror', (err) => { + pageErrors.push(`Page error: ${err.message}\nStack: ${err.stack || 'No stack trace'}`); + }); + + await page.goto(`http://localhost:${port}`); + + // Wait for the page to load but don't wait for networkidle since env loading might be polling + await page.waitForLoadState('load'); + + // Wait for React to render - either the loading screen or the main content + await page.waitForSelector('body > div', { timeout: 10000 }); + + // Log any errors found + if (pageErrors.length > 0) { + console.log('=== PAGE ERRORS ==='); + pageErrors.forEach(error => console.log(error)); + console.log('=================='); + } + + if (consoleMessages.length > 0) { + console.log('=== CONSOLE MESSAGES ==='); + consoleMessages.forEach(msg => console.log(msg)); + console.log('========================'); + } +} + +async function waitForEnvironmentLoading(page: Page) { + // Wait for either the loading screen to disappear or main content to appear + // The loading screen shows "Loading environment configuration..." + const loadingText = page.locator('text=Loading environment configuration...'); + const mainContent = page.locator('h1'); + + try { + // Wait up to 15 seconds for either loading to finish or main content to appear + await Promise.race([ + loadingText.waitFor({ state: 'hidden', timeout: 15000 }), + mainContent.waitFor({ state: 'visible', timeout: 15000 }) + ]); + } catch (error) { + console.log('Environment loading timeout - checking current page state'); + const pageContent = await page.content(); + console.log('Current page content length:', pageContent.length); + + // If still loading, wait a bit more and proceed + if (await loadingText.isVisible()) { + console.log('Still showing loading screen, waiting 10 more seconds...'); + await page.waitForTimeout(10000); + } + } +} + +async function checkElementWithTextPresence(page: Page, selector: string, text: string) { + const element = page.locator(`${selector}:has-text("${text}")`); + await expect(element).toBeVisible(); +} + +async function clickElementWithText(page: Page, selector: string, text: string) { + await page.click(`${selector}:has-text("${text}")`); +} + +async function checkDateFormat(page: Page) { + const dateElement = page.locator('text=/[A-Z][a-z]+ \\d{1,2}[a-z]{2} \\d{4}, \\d{1,2}:\\d{2}/').first(); + await expect(dateElement).toBeVisible(); +} + +const appsData = [ + { + header: 'Dynamic Remotes with Runtime Environment Variables', + subheader: 'Host', + hostH3: 'Environment Configuration:', + paragraph: 'This example demonstrates how Module Federation can load remote components dynamically', + button: 'Load Remote Widget', + buttonH2: 'Remote Widget', + buttonParagraph: 'Using momentjs for format the date', + host: 3000, + }, + { + header: 'Dynamic System Host', + subheader: 'Remote', + buttonH2: 'Remote Widget', + buttonParagraph: 'Using momentjs for format the date', + host: 3001, + }, +]; + +test.describe('Dynamic Remotes Runtime Environment Variables E2E Tests', () => { + + appsData.forEach((appData) => { + const { host, subheader, header, hostH3, paragraph, button, buttonH2, buttonParagraph } = appData; + + test.describe(`Check ${subheader} app`, () => { + test(`should display ${subheader} app widget functionality and application elements`, async ({ page }) => { + await openLocalhost(page, host); + + // Wait for environment loading to complete for host app + if (host === 3000) { + await waitForEnvironmentLoading(page); + } + + // Check main header + await checkElementWithTextPresence(page, 'h1', header); + + if (host === 3000) { + // Host app specific elements + await checkElementWithTextPresence(page, 'h3', hostH3!); + await checkElementWithTextPresence(page, 'p', paragraph!); + + // Click the load remote component button + await clickElementWithText(page, 'button', button!); + + // Wait for loading to complete + await page.waitForTimeout(3000); + } + + // Check that the remote component loaded successfully + await checkElementWithTextPresence(page, 'h2', buttonH2); + await checkElementWithTextPresence(page, 'p', buttonParagraph); + + // Check moment.js date formatting + await checkDateFormat(page); + }); + }); + }); + + test.describe('Runtime Environment Variable Tests', () => { + test('should load environment configuration successfully', async ({ page }) => { + const networkRequests: string[] = []; + + page.on('request', (request) => { + networkRequests.push(request.url()); + }); + + await page.goto('http://localhost:3000'); + await page.waitForLoadState('load'); + await waitForEnvironmentLoading(page); + + // Check that env-config.json was loaded + const envConfigRequests = networkRequests.filter(url => + url.includes('env-config.json') + ); + + expect(envConfigRequests.length).toBeGreaterThan(0); + }); + + test('should demonstrate dynamic remote loading with environment config', async ({ page }) => { + await openLocalhost(page, 3000); + await waitForEnvironmentLoading(page); + + // Click to load remote component + await clickElementWithText(page, 'button', 'Load Remote Widget'); + + // Wait for loading to complete + await page.waitForTimeout(3000); + + // Verify remote component is now loaded + await checkElementWithTextPresence(page, 'h2', 'Remote Widget'); + await checkElementWithTextPresence(page, 'p', 'Using momentjs for format the date'); + }); + }); + + test.describe('Performance and Loading', () => { + test('should load applications within reasonable time', async ({ page }) => { + const startTime = Date.now(); + + await page.goto('http://localhost:3000'); + await page.waitForLoadState('load'); + await waitForEnvironmentLoading(page); + + const loadTime = Date.now() - startTime; + expect(loadTime).toBeLessThan(10000); // Should load within 10 seconds + }); + }); +}); \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts new file mode 100644 index 00000000000..3582d78f830 --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/base-test.ts @@ -0,0 +1,46 @@ +import { Page } from '@playwright/test'; + +export class BasePage { + constructor(public page: Page) {} + + async openLocalhost(port: number) { + await this.page.goto(`http://localhost:${port}`); + await this.page.waitForLoadState('networkidle'); + } + + async checkElementWithTextPresence(selector: string, text: string, timeout: number = 10000) { + await this.page.locator(selector).filter({ hasText: text }).waitFor({ timeout }); + } + + async checkElementVisibility(selector: string, timeout: number = 10000) { + await this.page.locator(selector).waitFor({ state: 'visible', timeout }); + } + + async checkElementHidden(selector: string, text: string, timeout: number = 10000) { + await this.page.locator(selector).filter({ hasText: text }).waitFor({ state: 'hidden', timeout }); + } + + async clickElementWithText(selector: string, text: string) { + await this.page.locator(selector).filter({ hasText: text }).click(); + } + + async waitForDynamicImport(timeout: number = 5000) { + // Wait for any dynamic imports to complete + await this.page.waitForTimeout(1000); + await this.page.waitForLoadState('networkidle', { timeout }); + } + + async checkDateFormat() { + // Check for moment.js formatted date (MMMM Do YYYY, h:mm format) + const dateElement = this.page.locator('text=/[A-Z][a-z]+ \\d{1,2}[a-z]{2} \\d{4}, \\d{1,2}:\\d{2}/'); + await dateElement.waitFor({ timeout: 5000 }); + } + + async waitForLoadingToDisappear(loadingText: string, timeout: number = 10000) { + // Wait for loading text to appear first + await this.page.locator(`text=${loadingText}`).waitFor({ timeout: 5000 }); + // Then wait for it to disappear + await this.page.locator(`text=${loadingText}`).waitFor({ state: 'hidden', timeout }); + } +} + diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts new file mode 100644 index 00000000000..a9cd28aba69 --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/constants.ts @@ -0,0 +1,22 @@ +export const Constants = { + elementsText: { + dynamicSystemRemotesRuntimeApp: { + host: { + header: 'Dynamic Remotes with Runtime Environment Variables', + hostH3: 'Environment Configuration:', + button: 'Load Remote Widget', + }, + paragraph: 'This example demonstrates how Module Federation can load remote components dynamically', + buttonHeader: 'Remote Component:', + buttonH2: 'Remote Widget', + buttonParagraph: 'Using momentjs for format the date', + }, + }, + commonConstantsData: { + basicComponents: { + host: 'Host', + remote: 'Remote', + }, + loading: 'Loading...', + }, +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/selectors.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/selectors.ts new file mode 100644 index 00000000000..9d3d84660a7 --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/e2e/utils/selectors.ts @@ -0,0 +1,14 @@ +export const selectors = { + tags: { + headers: { + h1: 'h1', + h2: 'h2', + h3: 'h3', + }, + coreElements: { + button: 'button', + div: 'div', + }, + paragraph: 'p', + }, +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/Dockerfile b/advanced-api/dynamic-remotes-runtime-environment-variables/host/Dockerfile index 5f399e3714f..4131b53f409 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/Dockerfile +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/Dockerfile @@ -1,35 +1,79 @@ -# => Build container -FROM node:alpine as builder +# Multi-stage build for security and optimization +# Build stage +FROM node:18-alpine AS builder + +# Set working directory WORKDIR /app -COPY package.json . -COPY yarn.lock . -RUN yarn + +# Add security patches and necessary packages +RUN apk add --no-cache \ + dumb-init \ + && rm -rf /var/cache/apk/* + +# Copy package files first for better caching +COPY package*.json ./ +COPY yarn.lock* ./ + +# Install dependencies with security and performance optimizations +RUN yarn install --frozen-lockfile --production=false \ + && yarn cache clean + +# Copy source code COPY . . + +# Build the application RUN yarn build -# => Run container -FROM nginx:1.27.0-alpine +# Production stage +FROM nginx:1.27.0-alpine AS production + +# Install security updates and required packages +RUN apk upgrade --no-cache \ + && apk add --no-cache \ + dumb-init \ + bash \ + curl \ + && rm -rf /var/cache/apk/* -# Nginx config -RUN rm -rf /etc/nginx/conf.d +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs \ + && adduser -S nextjs -u 1001 -G nodejs + +# Remove default nginx config and add security headers +RUN rm -rf /etc/nginx/conf.d/default.conf + +# Copy custom nginx configuration COPY conf /etc/nginx -# Static build -COPY --from=builder /app/dist /usr/share/nginx/html/ +# Copy built application from builder stage +COPY --from=builder --chown=nextjs:nodejs /app/dist /usr/share/nginx/html/ -# Default port exposure -EXPOSE 80 +# Copy environment configuration files +COPY --chown=nextjs:nodejs env-config.json /usr/share/nginx/html/ +COPY --chown=nextjs:nodejs env.sh /usr/share/nginx/html/ +COPY --chown=nextjs:nodejs .env /usr/share/nginx/html/ + +# Make shell script executable +RUN chmod +x /usr/share/nginx/html/env.sh -# Copy .env file and shell script to container -WORKDIR /usr/share/nginx/html -COPY ./env.sh . -COPY .env . +# Create nginx run directory and set permissions +RUN mkdir -p /var/run/nginx \ + && chown -R nextjs:nodejs /var/run/nginx \ + && chown -R nextjs:nodejs /var/cache/nginx \ + && chown -R nextjs:nodejs /usr/share/nginx/html -# Add bash -RUN apk add --no-cache bash +# Add health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/env-config.json || exit 1 + +# Use non-root user +USER nextjs + +# Expose port +EXPOSE 80 -# Make our shell script executable -RUN chmod +x env.sh +# Use dumb-init for proper signal handling +ENTRYPOINT ["dumb-init", "--"] -# Start Nginx server -CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""] +# Start command with environment setup +CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/conf/conf.d/default.conf b/advanced-api/dynamic-remotes-runtime-environment-variables/host/conf/conf.d/default.conf index 522821cfb47..64f5f2ffa9d 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/conf/conf.d/default.conf +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/conf/conf.d/default.conf @@ -1,37 +1,113 @@ server { - listen 80; - location / { - # Enable wide open CORS, don't actually do that in production environment - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; + listen 80; + server_name localhost; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3001; style-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:3001; img-src 'self' data:; font-src 'self';" always; + + # Remove server signature + server_tokens off; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Main location block + location / { + # CORS headers for development (restrict in production) + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + add_header 'Access-Control-Max-Age' 1728000 always; + add_header 'Content-Type' 'text/plain charset=UTF-8' always; + add_header 'Content-Length' 0 always; + return 204; + } + + if ($request_method = 'POST') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + } + + if ($request_method = 'GET') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + } + + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # No cache for HTML files + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } } - if ($request_method = 'POST') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + + # Special handling for env-config.json (never cache) + location = /env-config.json { + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + + # CORS for env config + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Cache-Control,Content-Type' always; + + try_files $uri =404; } - if ($request_method = 'GET') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + + # Health check endpoint + location = /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; } - - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - expires -1; # Set it to different value depending on your standard requirements - } - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } -} + + # Error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + internal; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location ~ /env\.sh$ { + deny all; + access_log off; + log_not_found off; + } +} \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/env-config.json b/advanced-api/dynamic-remotes-runtime-environment-variables/host/env-config.json index 65e5d7a7452..73762173e84 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/env-config.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/env-config.json @@ -1,3 +1,7 @@ { - "API_URL": "https://host.api.com" -} + "API_URL": "https://host.api.com", + "REMOTE_URL": "http://localhost:3001/remoteEntry.js", + "NODE_ENV": "development", + "VERSION": "1.0.0", + "LAST_UPDATED": "2024-01-01T00:00:00.000Z" +} \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/package.json b/advanced-api/dynamic-remotes-runtime-environment-variables/host/package.json index d44809eeb1f..3c27fd32bf4 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/package.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/package.json @@ -6,6 +6,7 @@ "@babel/preset-react": "7.24.7", "@module-federation/enhanced": "0.17.1", "@module-federation/runtime": "0.17.1", + "@module-federation/retry-plugin": "0.17.1", "@rspack/cli": "1.4.11", "@rspack/core": "1.4.11", "@rspack/dev-server": "1.1.3", @@ -14,23 +15,24 @@ "html-webpack-plugin": "5.6.0", "serve": "14.2.3", "webpack": "5.101.0", + "webpack-merge": "6.0.1", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" }, "scripts": { - "start": "chmod +x ./env.sh && cp env-config.json ./public/ && rspack serve", - "build": "rspack build --mode production", - "serve": "serve dist -p 3001", + "start": "chmod +x ./env.sh && cp env-config.json ./public/ && NODE_ENV=development rspack serve", + "build": "NODE_ENV=production rspack build --mode production", + "serve": "serve dist -p 3000", "clean": "rm -rf dist", "docker:build": "docker build . -t csr-env/host:0.0.0", "docker:run": "docker run -it --name csr-env-host -p 3000:80 -d -e API_URL=https://host.com/api csr-env/host:0.0.0", "docker:rm": "docker rm -f csr-env-host", - "legacy:start": "chmod +x ./env.sh && cp env-config.json ./public/ && webpack-cli serve ", - "legacy:build": "webpack --mode production" + "legacy:start": "chmod +x ./env.sh && cp env-config.json ./public/ && NODE_ENV=development webpack-cli serve", + "legacy:build": "NODE_ENV=production webpack --mode production" }, "dependencies": { - "moment": "^2.29.4", - "react": "^16.13.0", - "react-dom": "^16.13.0" + "moment": "^2.30.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" } } \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/public/env-config.json b/advanced-api/dynamic-remotes-runtime-environment-variables/host/public/env-config.json index 65e5d7a7452..73762173e84 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/public/env-config.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/public/env-config.json @@ -1,3 +1,7 @@ { - "API_URL": "https://host.api.com" -} + "API_URL": "https://host.api.com", + "REMOTE_URL": "http://localhost:3001/remoteEntry.js", + "NODE_ENV": "development", + "VERSION": "1.0.0", + "LAST_UPDATED": "2024-01-01T00:00:00.000Z" +} \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/rspack.config.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/rspack.config.js index 54b88e899ee..35316d25120 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/rspack.config.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/rspack.config.js @@ -1,16 +1,18 @@ const { CopyRspackPlugin, } = require('@rspack/core'); -const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack') +const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack'); const deps = require('./package.json').dependencies; const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const isProduction = process.env.NODE_ENV === 'production'; + module.exports = { - mode: 'development', + mode: isProduction ? 'production' : 'development', entry: './src/index', - devtool: 'source-map', + devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map', devServer: { static: { directory: path.join(__dirname, 'dist'), @@ -21,9 +23,15 @@ module.exports = { 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, port: 3000, + hot: true, + compress: true, + historyApiFallback: true, }, output: { - publicPath: 'auto', + publicPath: '/', + clean: true, + filename: isProduction ? '[name].[contenthash].js' : '[name].js', + chunkFilename: isProduction ? '[name].[contenthash].js' : '[name].js', }, module: { rules: [ @@ -41,6 +49,7 @@ module.exports = { transform: { react: { runtime: 'automatic', + development: !isProduction, }, }, }, @@ -51,22 +60,69 @@ module.exports = { }, plugins: [ new CopyRspackPlugin({ - patterns: [{ from: 'public/env-config.json', to: 'env-config.json' }], + patterns: [ + { + from: 'public/env-config.json', + to: 'env-config.json', + noErrorOnMissing: true + } + ], }), new ModuleFederationPlugin({ name: 'host', + runtimePlugins: [], shared: { - ...deps, react: { singleton: true, + requiredVersion: '^18.0.0', + eager: false, }, 'react-dom': { singleton: true, + requiredVersion: '^18.0.0', + eager: false, + }, + moment: { + singleton: false, }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', + inject: true, + minify: isProduction && { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, }), ], + resolve: { + extensions: ['.js', '.jsx'], + }, + optimization: isProduction ? { + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\/\\]node_modules[\/\\]/, + name: 'vendors', + chunks: 'all', + }, + }, + }, + minimize: true, + } : {}, + performance: isProduction ? { + hints: 'warning', + maxEntrypointSize: 512000, + maxAssetSize: 512000, + } : false, }; diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/bootstrap.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/bootstrap.js index db98af8e938..25aa84c3135 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/bootstrap.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/bootstrap.js @@ -1,5 +1,30 @@ import App from './components/App'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; +import ErrorBoundary from './components/ErrorBoundary'; -ReactDOM.render(, document.getElementById('root')); +// React 18 createRoot API +const container = document.getElementById('root'); +const root = createRoot(container); + +root.render( + + + + + +); + +// Performance monitoring +if (typeof window !== 'undefined' && window.performance) { + window.addEventListener('load', () => { + setTimeout(() => { + const perfData = performance.getEntriesByType('navigation')[0]; + console.log('Host App Performance:', { + loadTime: Math.round(perfData.loadEventEnd - perfData.loadEventStart), + domContentLoaded: Math.round(perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart), + totalTime: Math.round(perfData.loadEventEnd - perfData.fetchStart) + }); + }, 0); + }); +} diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/App.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/App.js index 3658dd9b984..f82fed1817e 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/App.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/App.js @@ -5,15 +5,109 @@ import useFetchJson from '../hooks/useFetchJson'; export const EnvContext = createContext(); const App = () => { - const { data, loading } = useFetchJson(`${__webpack_public_path__}env-config.json`); - return loading ? ( - 'Loading...' - ) : ( - <> - -
- - + const { data, loading, error, retry } = useFetchJson( + '/env-config.json', + { + maxRetries: 2, + retryDelay: 500, + timeout: 3000, + validateData: (data) => data && typeof data === 'object', + fallbackData: { + API_URL: 'https://fallback.api.com', + REMOTE_URL: 'http://localhost:3001/remoteEntry.js' + } + } + ); + + if (loading) { + return ( +
+
+
+
Loading environment configuration...
+ +
+
+ ); + } + + if (error && !data) { + return ( +
+
+

Configuration Error

+

+ Failed to load environment configuration: {error.message} +

+ +
+
+ ); + } + + return ( + + {error && ( +
+ Warning: Using fallback configuration due to loading error: {error.message} +
+ )} +
+ ); }; diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/ErrorBoundary.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/ErrorBoundary.js new file mode 100644 index 00000000000..36242fcc6f8 --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/ErrorBoundary.js @@ -0,0 +1,109 @@ +import React from 'react'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // Log error details for debugging + console.error('ErrorBoundary caught an error:', error, errorInfo); + + this.setState({ + error: error, + errorInfo: errorInfo + }); + + // Report error to monitoring service in production + if (process.env.NODE_ENV === 'production') { + // Example: reportError(error, errorInfo); + } + } + + handleRetry = () => { + this.setState({ hasError: false, error: null, errorInfo: null }); + }; + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

+ An error occurred while loading the application. This could be due to: +

+
    +
  • Network connectivity issues
  • +
  • Remote module loading failures
  • +
  • Configuration problems
  • +
+ +
+ + View technical details + +
+              {this.state.error && this.state.error.toString()}
+              {this.state.errorInfo.componentStack}
+            
+
+ +
+ + +
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/Main.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/Main.js index be1ef3255f6..9d4288a388c 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/Main.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/components/Main.js @@ -1,41 +1,194 @@ -import React, { useContext, useState } from 'react'; -import { useFederatedComponent } from '../hooks/useFederatedComponent'; +import React, { useContext, useState, useCallback } from 'react'; +import { useFederatedComponent, preloadRemote } from '../hooks/useFederatedComponent'; import { EnvContext } from './App'; const Main = () => { const ENV = useContext(EnvContext); const [{ module, scope, url }, setSystem] = useState({}); + const [isPreloading, setIsPreloading] = useState(false); - const loadRemoteWidget = () => { + const loadRemoteWidget = useCallback(() => { setSystem({ - url: 'http://localhost:3001/remoteEntry.js', + url: ENV?.REMOTE_URL || 'http://localhost:3001/remoteEntry.js', scope: 'remote', module: './Widget', }); - }; + }, [ENV?.REMOTE_URL]); + + const preloadWidget = useCallback(async () => { + setIsPreloading(true); + try { + const remoteUrl = ENV?.REMOTE_URL || 'http://localhost:3001/remoteEntry.js'; + await preloadRemote(remoteUrl, 'remote', './Widget'); + console.log('Widget preloaded successfully'); + } catch (error) { + console.error('Failed to preload widget:', error); + } finally { + setIsPreloading(false); + } + }, [ENV?.REMOTE_URL]); + + const { + Component: FederatedComponent, + loading, + error, + retry, + retryCount, + clearCache + } = useFederatedComponent(url, scope, module, { + maxRetries: 3, + retryDelay: 1000, + timeout: 10000 + }); - const { Component: FederatedComponent, errorLoading } = useFederatedComponent(url, scope, module); + const styles = { + container: { + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', + padding: '20px', + maxWidth: '800px', + margin: '0 auto' + }, + header: { + color: '#333', + borderBottom: '2px solid #007acc', + paddingBottom: '10px' + }, + envInfo: { + backgroundColor: '#f5f5f5', + padding: '10px', + borderRadius: '4px', + margin: '10px 0', + fontSize: '14px' + }, + buttonGroup: { + display: 'flex', + gap: '10px', + margin: '20px 0' + }, + button: { + backgroundColor: '#007acc', + color: 'white', + border: 'none', + padding: '10px 20px', + borderRadius: '4px', + cursor: 'pointer', + fontSize: '14px' + }, + buttonSecondary: { + backgroundColor: '#6c757d', + color: 'white', + border: 'none', + padding: '10px 20px', + borderRadius: '4px', + cursor: 'pointer', + fontSize: '14px' + }, + buttonDisabled: { + backgroundColor: '#ccc', + cursor: 'not-allowed' + }, + errorContainer: { + backgroundColor: '#ffebee', + border: '1px solid #f44336', + borderRadius: '4px', + padding: '15px', + margin: '10px 0' + }, + errorText: { + color: '#d32f2f', + marginBottom: '10px' + }, + remoteContainer: { + marginTop: '2em', + border: '1px solid #ddd', + borderRadius: '8px', + padding: '20px' + }, + statusText: { + fontSize: '14px', + color: '#666', + margin: '10px 0' + } + }; return ( -
-

Dynamic System Host

-

Host

-

my env is {ENV.API_URL}

+
+

Dynamic Remotes with Runtime Environment Variables

+ +
+

Environment Configuration:

+
    +
  • API URL: {ENV?.API_URL || 'Not configured'}
  • +
  • Remote URL: {ENV?.REMOTE_URL || 'http://localhost:3001/remoteEntry.js (default)'}
  • +
  • Environment: {process.env.NODE_ENV || 'development'}
  • +
+
+

- The Dynamic System will take advantage Module Federation remotes and{' '} - exposes. It will not load components that have been loaded already. + This example demonstrates how Module Federation can load remote components dynamically + with runtime environment variables. The remote URL and other configuration can be + changed without rebuilding the application.

- -
- - {errorLoading - ? `Error loading module "${module}"` - : FederatedComponent && } + +
+ + + + + {FederatedComponent && ( + + )} +
+ + {error && ( +
+
+ Error loading remote component: {error.message} +
+
+ Failed after {retryCount} attempts +
+ +
+ )} + + {loading && ( +
+ Loading remote component... This may take a few seconds. +
+ )} + +
+

Remote Component:

+ Initializing remote component...
+ }> + {FederatedComponent ? ( + + ) : ( +
+ No remote component loaded. Click "Load Remote Widget" to begin. +
+ )}
diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFederatedComponent.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFederatedComponent.js index 04cf04543f1..1fe15c93c30 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFederatedComponent.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFederatedComponent.js @@ -1,44 +1,256 @@ -import React, { lazy, useEffect, useState } from 'react'; -import {registerRemotes, loadRemote} from "@module-federation/runtime"; +import { lazy, useEffect, useState, useCallback, useRef } from 'react'; +import { registerRemotes, loadRemote } from '@module-federation/runtime'; -function loadComponent(scope, module) { +// Enhanced loading function with retry logic +function loadComponent(scope, module, maxRetries = 3, retryDelay = 1000) { return async () => { - if(!scope && !module) { - return {default: ()=>null} + if (!scope || !module) { + console.warn('useFederatedComponent: scope and module are required'); + return { default: () => null }; } - return await loadRemote(scope + module.replace('.','')); + + let lastError; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(`Loading remote: ${scope}${module.replace('.', '')} (attempt ${attempt}/${maxRetries})`); + const result = await loadRemote(scope + module.replace('.', '')); + + if (result) { + console.log(`Successfully loaded remote: ${scope}${module.replace('.', '')}`); + return result; + } + throw new Error('Remote loaded but returned null/undefined'); + } catch (error) { + lastError = error; + console.warn(`Failed to load remote (attempt ${attempt}/${maxRetries}):`, error.message); + + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, retryDelay * attempt)); + } + } + } + + throw new Error(`Failed to load remote ${scope}${module.replace('.', '')} after ${maxRetries} attempts: ${lastError.message}`); }; } -const componentCache = new Map(); -export const useFederatedComponent = (remoteUrl, scope, module) => { - console.log(remoteUrl,scope, module); - if(scope && remoteUrl) { - registerRemotes([ - { - name: scope, - entry: remoteUrl, - }, - ]); +// Component cache with TTL (Time To Live) +class ComponentCache { + constructor() { + this.cache = new Map(); + this.timestamps = new Map(); + this.TTL = 5 * 60 * 1000; // 5 minutes } - const key = `${remoteUrl}-${scope}-${module}`; + + get(key) { + const timestamp = this.timestamps.get(key); + if (timestamp && Date.now() - timestamp > this.TTL) { + this.delete(key); + return null; + } + return this.cache.get(key); + } + + set(key, value) { + this.cache.set(key, value); + this.timestamps.set(key, Date.now()); + } + + delete(key) { + this.cache.delete(key); + this.timestamps.delete(key); + } + + clear() { + this.cache.clear(); + this.timestamps.clear(); + } +} + +const componentCache = new ComponentCache(); + +export const useFederatedComponent = ( + remoteUrl, + scope, + module, + options = {} +) => { + const { + maxRetries = 3, + retryDelay = 1000, + enableCache = true, + timeout = 10000 + } = options; + const [Component, setComponent] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const abortControllerRef = useRef(null); + + const key = `${remoteUrl}-${scope}-${module}`; + + console.log('useFederatedComponent:', { remoteUrl, scope, module, key }); + + // Register remote if URL and scope are provided + useEffect(() => { + if (scope && remoteUrl) { + try { + registerRemotes([ + { + name: scope, + entry: remoteUrl, + }, + ]); + console.log(`Registered remote: ${scope} at ${remoteUrl}`); + } catch (err) { + console.error(`Failed to register remote ${scope}:`, err); + setError(err); + } + } + }, [scope, remoteUrl]); + + const loadComponentWithTimeout = useCallback(async () => { + if (!scope || !module || !remoteUrl) { + return; + } + + // Cancel previous request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + setLoading(true); + setError(null); + + try { + // Check cache first + if (enableCache) { + const cachedComponent = componentCache.get(key); + if (cachedComponent) { + console.log(`Using cached component for: ${key}`); + setComponent(cachedComponent); + setLoading(false); + return; + } + } + + // Create timeout promise + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout) + ); + + // Create abort promise + const abortPromise = new Promise((_, reject) => { + signal.addEventListener('abort', () => { + reject(new Error('Request aborted')); + }); + }); + + // Load component with timeout and abort capability + const componentLoader = loadComponent(scope, module, maxRetries, retryDelay); + const loadPromise = componentLoader(); + + const result = await Promise.race([loadPromise, timeoutPromise, abortPromise]); + + if (signal.aborted) { + return; + } + + const LazyComponent = lazy(() => Promise.resolve(result)); + + if (enableCache) { + componentCache.set(key, LazyComponent); + } + + setComponent(LazyComponent); + setRetryCount(0); + } catch (err) { + if (signal.aborted) { + return; + } + + console.error(`Error loading federated component:`, err); + setError(err); + setRetryCount(prev => prev + 1); + } finally { + if (!signal.aborted) { + setLoading(false); + } + } + }, [scope, module, remoteUrl, key, maxRetries, retryDelay, enableCache, timeout]); + // Reset component when key changes useEffect(() => { - if (Component) setComponent(null); - // Only recalculate when key changes + if (Component) { + setComponent(null); + } }, [key]); + // Load component useEffect(() => { - if (!Component) { - const Comp = lazy(loadComponent(scope, module)); - componentCache.set(key, Comp); - setComponent(Comp); + if (!Component && !loading && scope && module && remoteUrl) { + loadComponentWithTimeout(); } - // key includes all dependencies (scope/module) - }, [Component, key]); + }, [Component, loading, loadComponentWithTimeout]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + + const retry = useCallback(() => { + setError(null); + setComponent(null); + loadComponentWithTimeout(); + }, [loadComponentWithTimeout]); + + const clearCache = useCallback(() => { + componentCache.delete(key); + setComponent(null); + setError(null); + }, [key]); - return { Component }; + return { + Component, + loading, + error, + retry, + retryCount, + clearCache, + // Legacy compatibility + errorLoading: error + }; +}; + +// Utility function to preload a remote +export const preloadRemote = async (remoteUrl, scope, module, options = {}) => { + const { maxRetries = 3, retryDelay = 1000 } = options; + + try { + if (scope && remoteUrl) { + registerRemotes([ + { + name: scope, + entry: remoteUrl, + }, + ]); + } + + const componentLoader = loadComponent(scope, module, maxRetries, retryDelay); + await componentLoader(); + console.log(`Preloaded remote: ${scope}${module.replace('.', '')}`); + } catch (error) { + console.error(`Failed to preload remote:`, error); + throw error; + } }; -export default useFederatedComponent; +export default useFederatedComponent; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFetchJson.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFetchJson.js index 7a35f722c3c..1193494c7eb 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFetchJson.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/src/hooks/useFetchJson.js @@ -1,28 +1,149 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; + +const useFetchJson = (path, options = {}) => { + const { + maxRetries = 3, + retryDelay = 1000, + timeout = 5000, + validateData = null, + fallbackData = null + } = options; -const useFetchJson = path => { const [loading, setIsLoading] = useState(true); const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const abortControllerRef = useRef(null); + const isMountedRef = useRef(true); + + const fetchData = useCallback(async () => { + if (!path) { + console.warn('useFetchJson: path is required'); + setIsLoading(false); + return; + } + + // Cancel previous request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + setIsLoading(true); + setError(null); + + let lastError; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(`Fetching JSON from ${path} (attempt ${attempt}/${maxRetries})`); + + // Create timeout promise + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout) + ); + + // Create fetch promise + const fetchPromise = fetch(path, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'Cache-Control': 'no-cache' + }, + signal + }); + + const response = await Promise.race([fetchPromise, timeoutPromise]); + + if (signal.aborted || !isMountedRef.current) { + return; + } + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + console.warn('Response is not JSON, attempting to parse anyway'); + } - const fetchData = () => { - fetch(path, { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }) - .then(res => res.json()) - .then(json => { + const json = await response.json(); + + // Validate data if validator provided + if (validateData && !validateData(json)) { + throw new Error('Data validation failed'); + } + + if (!isMountedRef.current) { + return; + } + + console.log(`Successfully fetched JSON from ${path}`); setData(json); + setRetryCount(0); setIsLoading(false); - }); - }; + return; + + } catch (err) { + lastError = err; + + if (signal.aborted || !isMountedRef.current) { + return; + } + + console.warn(`Failed to fetch JSON (attempt ${attempt}/${maxRetries}):`, err.message); + + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, retryDelay * attempt)); + } + } + } + + // All retries failed + if (!signal.aborted && isMountedRef.current) { + console.error(`Failed to fetch JSON from ${path} after ${maxRetries} attempts:`, lastError); + setError(lastError); + setRetryCount(maxRetries); + + // Use fallback data if provided + if (fallbackData !== null) { + console.log('Using fallback data'); + setData(fallbackData); + } + + setIsLoading(false); + } + }, [path, maxRetries, retryDelay, timeout, validateData, fallbackData]); + + const retry = useCallback(() => { + setError(null); + setRetryCount(0); + fetchData(); + }, [fetchData]); useEffect(() => { fetchData(); + }, [fetchData]); + + // Cleanup on unmount + useEffect(() => { + return () => { + isMountedRef.current = false; + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; }, []); - return { data, loading }; + return { + data, + loading, + error, + retry, + retryCount + }; }; export default useFetchJson; diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/host/webpack.config.js b/advanced-api/dynamic-remotes-runtime-environment-variables/host/webpack.config.js index 1b62342cdf6..e84081e3118 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/host/webpack.config.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/host/webpack.config.js @@ -1,53 +1,59 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin; +const { ModuleFederationPlugin } = require('@module-federation/enhanced'); const CopyPlugin = require('copy-webpack-plugin'); const path = require('path'); module.exports = { entry: './src/index', mode: 'development', - devtool: 'source-map', devServer: { static: { directory: path.join(__dirname, 'dist'), }, + port: 3000, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, - port: 3000, - }, - output: { - publicPath: 'auto', }, module: { rules: [ { test: /\.jsx?$/, - loader: 'babel-loader', exclude: /node_modules/, - options: { - presets: ['@babel/preset-react'], + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-react'], + }, }, }, ], }, plugins: [ new CopyPlugin({ - patterns: [{ from: 'public/env-config.json', to: 'env-config.json' }], + patterns: [ + { + from: 'public/env-config.json', + to: 'env-config.json', + noErrorOnMissing: true + } + ], }), new ModuleFederationPlugin({ name: 'host', shared: { react: { - import: 'react', // the "react" package will be used a provided and fallback module - shareKey: 'react', // under this name the shared module will be placed in the share scope - shareScope: 'default', // share scope with this name will be used - singleton: true, // only a single version of the shared module is allowed + singleton: true, + requiredVersion: '^18.0.0', }, 'react-dom': { - singleton: true, // only a single version of the shared module is allowed + singleton: true, + requiredVersion: '^18.0.0', + }, + moment: { + singleton: false, }, }, }), diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/package.json b/advanced-api/dynamic-remotes-runtime-environment-variables/package.json index 32d9c3cc533..ac727d4febd 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/package.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/package.json @@ -15,10 +15,11 @@ "docker:build": "pnpm --filter dynamic-remotes-runtime-environment-variables_* --parallel docker:build", "docker:run": "pnpm --filter dynamic-remotes-runtime-environment-variables_* --parallel docker:run", "docker:rm": "pnpm --filter dynamic-remotes-runtime-environment-variables_* --parallel docker:rm", - "e2e:ci": "pnpm start & wait-on http-get://localhost:3000/ && npx cypress run --config-file ../../cypress-e2e/config/cypress.config.ts --config '{\"supportFile\": \"../../cypress-e2e/support/e2e.ts\"}' --spec \"./e2e/*.cy.ts\" --browser=chrome", - "legacy:e2e:ci": "pnpm legacy:start & wait-on http-get://localhost:3000/ && npx cypress run --config-file ../../cypress-e2e/config/cypress.config.ts --config '{\"supportFile\": \"../../cypress-e2e/support/e2e.ts\"}' --spec \"./e2e/*.cy.ts\" --browser=chrome" + "e2e:ci": "npx playwright test", + "legacy:e2e:ci": "USE_LEGACY=1 npx playwright test" }, "devDependencies": { + "@playwright/test": "^1.54.2", "wait-on": "7.2.0" } } diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/playwright.config.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/playwright.config.ts new file mode 100644 index 00000000000..2052cf7fe3e --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/playwright.config.ts @@ -0,0 +1,50 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 60000, + expect: { + timeout: 15000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['list'], + ], + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + viewport: { width: 1920, height: 1080 }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: [ + { + command: process.env.CI && process.env.USE_LEGACY + ? 'pnpm --filter dynamic-remotes-runtime-environment-variables_host legacy:start' + : 'pnpm --filter dynamic-remotes-runtime-environment-variables_host start', + port: 3000, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + { + command: process.env.CI && process.env.USE_LEGACY + ? 'pnpm --filter dynamic-remotes-runtime-environment-variables_remote legacy:start' + : 'pnpm --filter dynamic-remotes-runtime-environment-variables_remote start', + port: 3001, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + ], +}); \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/playwright.test.config.ts b/advanced-api/dynamic-remotes-runtime-environment-variables/playwright.test.config.ts new file mode 100644 index 00000000000..21a5511b93c --- /dev/null +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/playwright.test.config.ts @@ -0,0 +1,33 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 30000, + expect: { + timeout: 15000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['list'], + ], + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + viewport: { width: 1920, height: 1080 }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + // Disable webServer since we're running manually +}); \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/Dockerfile b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/Dockerfile index 5f399e3714f..43befa7759c 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/Dockerfile +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/Dockerfile @@ -1,35 +1,79 @@ -# => Build container -FROM node:alpine as builder +# Multi-stage build for security and optimization +# Build stage +FROM node:18-alpine AS builder + +# Set working directory WORKDIR /app -COPY package.json . -COPY yarn.lock . -RUN yarn + +# Add security patches and necessary packages +RUN apk add --no-cache \ + dumb-init \ + && rm -rf /var/cache/apk/* + +# Copy package files first for better caching +COPY package*.json ./ +COPY yarn.lock* ./ + +# Install dependencies with security and performance optimizations +RUN yarn install --frozen-lockfile --production=false \ + && yarn cache clean + +# Copy source code COPY . . + +# Build the application RUN yarn build -# => Run container -FROM nginx:1.27.0-alpine +# Production stage +FROM nginx:1.27.0-alpine AS production + +# Install security updates and required packages +RUN apk upgrade --no-cache \ + && apk add --no-cache \ + dumb-init \ + bash \ + curl \ + && rm -rf /var/cache/apk/* -# Nginx config -RUN rm -rf /etc/nginx/conf.d +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs \ + && adduser -S nextjs -u 1001 -G nodejs + +# Remove default nginx config and add security headers +RUN rm -rf /etc/nginx/conf.d/default.conf + +# Copy custom nginx configuration COPY conf /etc/nginx -# Static build -COPY --from=builder /app/dist /usr/share/nginx/html/ +# Copy built application from builder stage +COPY --from=builder --chown=nextjs:nodejs /app/dist /usr/share/nginx/html/ -# Default port exposure -EXPOSE 80 +# Copy environment configuration files +COPY --chown=nextjs:nodejs env-config.json /usr/share/nginx/html/ +COPY --chown=nextjs:nodejs env.sh /usr/share/nginx/html/ +COPY --chown=nextjs:nodejs .env /usr/share/nginx/html/ + +# Make shell script executable +RUN chmod +x /usr/share/nginx/html/env.sh -# Copy .env file and shell script to container -WORKDIR /usr/share/nginx/html -COPY ./env.sh . -COPY .env . +# Create nginx run directory and set permissions +RUN mkdir -p /var/run/nginx \ + && chown -R nextjs:nodejs /var/run/nginx \ + && chown -R nextjs:nodejs /var/cache/nginx \ + && chown -R nextjs:nodejs /usr/share/nginx/html -# Add bash -RUN apk add --no-cache bash +# Add health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/remoteEntry.js || exit 1 + +# Use non-root user +USER nextjs + +# Expose port +EXPOSE 80 -# Make our shell script executable -RUN chmod +x env.sh +# Use dumb-init for proper signal handling +ENTRYPOINT ["dumb-init", "--"] -# Start Nginx server -CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""] +# Start command with environment setup +CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g 'daemon off;'"] \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/conf/conf.d/default.conf b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/conf/conf.d/default.conf index 25e8e83ad31..521a3468d5d 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/conf/conf.d/default.conf +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/conf/conf.d/default.conf @@ -1,37 +1,123 @@ server { - listen 80; - location / { - # Enable wide open CORS, don't actually do that in production environment - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; + listen 80; + server_name localhost; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self';" always; + + # Remove server signature + server_tokens off; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Main location block + location / { + # CORS headers for module federation + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + add_header 'Access-Control-Max-Age' 1728000 always; + add_header 'Content-Type' 'text/plain charset=UTF-8' always; + add_header 'Content-Length' 0 always; + return 204; + } + + if ($request_method = 'POST') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + } + + if ($request_method = 'GET') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; + } + + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # No cache for HTML files + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } } - if ($request_method = 'POST') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + + # Special handling for remoteEntry.js (cache for short time) + location = /remoteEntry.js { + add_header Cache-Control "public, max-age=300" always; # 5 minutes + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Cache-Control,Content-Type' always; + + try_files $uri =404; } - if ($request_method = 'GET') { - add_header 'Access-Control-Allow-Origin' '*'; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + + # Special handling for env-config.json (never cache) + location = /env-config.json { + add_header Cache-Control "no-cache, no-store, must-revalidate" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + + # CORS for env config + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Cache-Control,Content-Type' always; + + try_files $uri =404; } - - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - expires -1; # Set it to different value depending on your standard requirements - } - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } -} + + # Health check endpoint + location = /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Error pages + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + internal; + } + + # Deny access to sensitive files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location ~ /env\.sh$ { + deny all; + access_log off; + log_not_found off; + } +} \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/env-config.json b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/env-config.json index 388a6ab2c36..f74ecf74f8a 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/env-config.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/env-config.json @@ -1,3 +1,6 @@ { - "API_URL": "https://default.api.com" + "API_URL": "https://remote.api.com", + "NODE_ENV": "development", + "VERSION": "1.0.0", + "LAST_UPDATED": "2024-01-01T00:00:00.000Z" } diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/package.json b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/package.json index c9251ce7571..bb201929eec 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/package.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/package.json @@ -5,6 +5,7 @@ "@babel/core": "7.24.7", "@babel/preset-react": "7.24.7", "@module-federation/enhanced": "0.17.1", + "@module-federation/retry-plugin": "0.17.1", "@rspack/cli": "1.4.11", "@rspack/core": "1.4.11", "@rspack/dev-server": "1.1.3", @@ -12,24 +13,25 @@ "html-webpack-plugin": "5.6.0", "serve": "14.2.3", "webpack": "5.101.0", + "webpack-merge": "6.0.1", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" }, "scripts": { - "start": "chmod +x ./env.sh && cp env-config.json ./public/ && rspack serve", - "build": "rspack build --mode production", + "start": "chmod +x ./env.sh && cp env-config.json ./public/ && NODE_ENV=development rspack serve", + "build": "NODE_ENV=production rspack build --mode production", "serve": "serve dist -p 3001", "clean": "rm -rf dist", "docker:build": "docker build . -t csr-env/remote:0.0.0", "docker:run": "docker run -it --name csr-env-remote -p 3001:80 -d -e API_URL=https://remote.com/api csr-env/remote:0.0.0", "docker:rm": "docker rm -f csr-env-remote", - "legacy:start": "chmod +x ./env.sh && cp env-config.json ./public/ && webpack-cli serve", - "legacy:build": "webpack --mode production" + "legacy:start": "chmod +x ./env.sh && cp env-config.json ./public/ && NODE_ENV=development webpack-cli serve", + "legacy:build": "NODE_ENV=production webpack --mode production" }, "dependencies": { "copy-webpack-plugin": "12.0.2", - "moment": "^2.29.4", - "react": "^16.13.0", - "react-dom": "^16.13.0" + "moment": "^2.30.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" } } \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/public/env-config.json b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/public/env-config.json index 388a6ab2c36..f74ecf74f8a 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/public/env-config.json +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/public/env-config.json @@ -1,3 +1,6 @@ { - "API_URL": "https://default.api.com" + "API_URL": "https://remote.api.com", + "NODE_ENV": "development", + "VERSION": "1.0.0", + "LAST_UPDATED": "2024-01-01T00:00:00.000Z" } diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/rspack.config.js b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/rspack.config.js index ebc9e48a1e9..a657cc16cd9 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/rspack.config.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/rspack.config.js @@ -1,24 +1,37 @@ const { + CopyRspackPlugin, } = require('@rspack/core'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack') +const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack'); const deps = require('./package.json').dependencies; +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +const isProduction = process.env.NODE_ENV === 'production'; module.exports = { + mode: isProduction ? 'production' : 'development', entry: './src/index', - mode: 'development', - devtool: 'source-map', + devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map', devServer: { - port: 3001, + static: { + directory: path.join(__dirname, 'dist'), + }, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, + port: 3001, + hot: true, + compress: true, + historyApiFallback: true, }, output: { publicPath: 'auto', + clean: true, + filename: isProduction ? '[name].[contenthash].js' : '[name].js', + chunkFilename: isProduction ? '[name].[contenthash].js' : '[name].js', }, module: { rules: [ @@ -36,6 +49,7 @@ module.exports = { transform: { react: { runtime: 'automatic', + development: !isProduction, }, }, }, @@ -45,29 +59,80 @@ module.exports = { ], }, plugins: [ + new CopyRspackPlugin({ + patterns: [ + { + from: 'public/env-config.json', + to: 'env-config.json', + noErrorOnMissing: true + } + ], + }), new ModuleFederationPlugin({ name: 'remote', filename: 'remoteEntry.js', exposes: { './Widget': './src/components/WidgetWrapper', }, + runtimePlugins: [], shared: { react: { - requiredVersion: deps.react, - import: 'react', - shareKey: 'react', - shareScope: 'default', singleton: true, + requiredVersion: '^18.0.0', + eager: false, }, 'react-dom': { - requiredVersion: deps['react-dom'], singleton: true, + requiredVersion: '^18.0.0', + eager: false, + }, + moment: { + singleton: false, + requiredVersion: deps.moment, }, - moment: deps.moment, }, }), new HtmlWebpackPlugin({ template: './public/index.html', + inject: true, + minify: isProduction && { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, }), ], -}; + resolve: { + extensions: ['.js', '.jsx'], + }, + optimization: isProduction ? { + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\/\\]node_modules[\/\\]/, + name: 'vendors', + chunks: 'all', + }, + shared: { + test: /[\/\\]shared[\/\\]/, + name: 'shared', + chunks: 'all', + }, + }, + }, + minimize: true, + } : {}, + performance: isProduction ? { + hints: 'warning', + maxEntrypointSize: 512000, + maxAssetSize: 512000, + } : false, +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/bootstrap.js b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/bootstrap.js index db98af8e938..ff0d6e747e1 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/bootstrap.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/bootstrap.js @@ -1,5 +1,27 @@ import App from './components/App'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -ReactDOM.render(, document.getElementById('root')); +// React 18 createRoot API +const container = document.getElementById('root'); +const root = createRoot(container); + +root.render( + + + +); + +// Performance monitoring for remote +if (typeof window !== 'undefined' && window.performance) { + window.addEventListener('load', () => { + setTimeout(() => { + const perfData = performance.getEntriesByType('navigation')[0]; + console.log('Remote App Performance:', { + loadTime: Math.round(perfData.loadEventEnd - perfData.loadEventStart), + domContentLoaded: Math.round(perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart), + totalTime: Math.round(perfData.loadEventEnd - perfData.fetchStart) + }); + }, 0); + }); +} diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/components/WidgetWrapper.js b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/components/WidgetWrapper.js index 4f1def47a15..5486059b0f7 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/components/WidgetWrapper.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/components/WidgetWrapper.js @@ -6,16 +6,101 @@ export const EnvContext = createContext(); // Wraps the Widget component with the EnvContext const WidgetWrapper = () => { - const { data, loading } = useFetchJson(`${__webpack_public_path__}env-config.json`); + const { data, loading, error, retry } = useFetchJson( + `${__webpack_public_path__}env-config.json`, + { + maxRetries: 3, + retryDelay: 1000, + timeout: 5000, + validateData: (data) => data && typeof data === 'object', + fallbackData: { + API_URL: 'https://remote.fallback.api.com' + } + } + ); + + if (loading) { + return ( +
+
+
+
Loading remote configuration...
+ +
+
+ ); + } + + if (error && !data) { + return ( +
+

Remote Configuration Error

+

+ Failed to load remote configuration: {error.message} +

+ +
+ ); + } - return loading ? ( - 'Loading...' - ) : ( - <> - - - - + return ( + + {error && ( +
+ Warning: Using fallback configuration: {error.message} +
+ )} + +
); }; diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/hooks/useFetchJson.js b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/hooks/useFetchJson.js index 2a61f6874f2..0f4559df56a 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/hooks/useFetchJson.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/src/hooks/useFetchJson.js @@ -1,27 +1,150 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; + +const useFetchJson = (path, options = {}) => { + const { + maxRetries = 3, + retryDelay = 1000, + timeout = 5000, + validateData = null, + fallbackData = null + } = options; -const useFetchJson = path => { const [loading, setIsLoading] = useState(true); const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const abortControllerRef = useRef(null); + const isMountedRef = useRef(true); - const fetchData = () => { - fetch(path, { - mode: 'no-cors', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }).then(json => { - setData(json); + const fetchData = useCallback(async () => { + if (!path) { + console.warn('useFetchJson: path is required'); setIsLoading(false); - }); - }; + return; + } + + // Cancel previous request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + setIsLoading(true); + setError(null); + + let lastError; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log(`Fetching JSON from ${path} (attempt ${attempt}/${maxRetries})`); + + // Create timeout promise + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error(`Request timeout after ${timeout}ms`)), timeout) + ); + + // Create fetch promise + const fetchPromise = fetch(path, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'Cache-Control': 'no-cache' + }, + signal + }); + + const response = await Promise.race([fetchPromise, timeoutPromise]); + + if (signal.aborted || !isMountedRef.current) { + setIsLoading(false); + return; + } + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + console.warn('Response is not JSON, attempting to parse anyway'); + } + + const json = await response.json(); + + // Validate data if validator provided + if (validateData && !validateData(json)) { + throw new Error('Data validation failed'); + } + + if (!isMountedRef.current) { + return; + } + + console.log(`Successfully fetched JSON from ${path}`); + setData(json); + setRetryCount(0); + setIsLoading(false); + return; + + } catch (err) { + lastError = err; + + if (signal.aborted || !isMountedRef.current) { + return; + } + + console.warn(`Failed to fetch JSON (attempt ${attempt}/${maxRetries}):`, err.message); + + if (attempt < maxRetries) { + await new Promise(resolve => setTimeout(resolve, retryDelay * attempt)); + } + } + } + + // All retries failed + if (!signal.aborted && isMountedRef.current) { + console.error(`Failed to fetch JSON from ${path} after ${maxRetries} attempts:`, lastError); + setError(lastError); + setRetryCount(maxRetries); + + // Use fallback data if provided + if (fallbackData !== null) { + console.log('Using fallback data'); + setData(fallbackData); + } + + setIsLoading(false); + } + }, [path, maxRetries, retryDelay, timeout, validateData, fallbackData]); + + const retry = useCallback(() => { + setError(null); + setRetryCount(0); + fetchData(); + }, [fetchData]); useEffect(() => { fetchData(); + }, [fetchData]); + + // Cleanup on unmount + useEffect(() => { + return () => { + isMountedRef.current = false; + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; }, []); - return { data, loading }; + return { + data, + loading, + error, + retry, + retryCount + }; }; -export default useFetchJson; +export default useFetchJson; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/webpack.config.js b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/webpack.config.js index b0fbc164fd0..062ce62b459 100644 --- a/advanced-api/dynamic-remotes-runtime-environment-variables/remote/webpack.config.js +++ b/advanced-api/dynamic-remotes-runtime-environment-variables/remote/webpack.config.js @@ -1,44 +1,46 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin; +const { ModuleFederationPlugin } = require('@module-federation/enhanced'); const CopyPlugin = require('copy-webpack-plugin'); -const path = require('path'); const deps = require('./package.json').dependencies; +const path = require('path'); module.exports = { entry: './src/index', mode: 'development', - devtool: 'source-map', devServer: { static: { directory: path.join(__dirname, 'dist'), }, port: 3001, headers: { - // Enable wide open CORS 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, }, - output: { - publicPath: 'auto', - }, module: { rules: [ { test: /\.jsx?$/, - loader: 'babel-loader', exclude: /node_modules/, - options: { - presets: ['@babel/preset-react'], + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-react'], + }, }, }, ], }, plugins: [ - // Exposes the env-config.json file new CopyPlugin({ - patterns: [{ from: 'public/env-config.json', to: 'env-config.json' }], + patterns: [ + { + from: 'public/env-config.json', + to: 'env-config.json', + noErrorOnMissing: true + } + ], }), new ModuleFederationPlugin({ name: 'remote', @@ -48,21 +50,21 @@ module.exports = { }, shared: { react: { - requiredVersion: deps.react, - import: 'react', // the "react" package will be used a provided and fallback module - shareKey: 'react', // under this name the shared module will be placed in the share scope - shareScope: 'default', // share scope with this name will be used - singleton: true, // only a single version of the shared module is allowed + singleton: true, + requiredVersion: '^18.0.0', }, 'react-dom': { - requiredVersion: deps['react-dom'], - singleton: true, // only a single version of the shared module is allowed + singleton: true, + requiredVersion: '^18.0.0', + }, + moment: { + singleton: false, + requiredVersion: deps.moment, }, - moment: deps.moment, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], -}; +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/README.md b/advanced-api/dynamic-remotes-synchronous-imports/README.md index ef5e3029f13..067c2a52cb8 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/README.md +++ b/advanced-api/dynamic-remotes-synchronous-imports/README.md @@ -1,24 +1,387 @@ -# Dynamic Remote with Vendor Sharing and Synchronous imports Example +# Dynamic Remotes with Synchronous Imports - Advanced Example -This example demos a basic host application loading remote component and sharing vendor code dynamically between unknown remotes +> **What makes this example unique**: This demonstrates how to use **runtime plugins** to dynamically change remote URLs while maintaining **synchronous imports** (like `import WidgetRemote from 'app2/Widget'`) instead of dynamic imports. -- `app1` standalone application which exposes `Widget` component. -- `app2` standalone application which exposes `Widget` component that requires - `momentjs`. +## 🎯 Core Concept -# Running Demo +This example showcases an advanced Module Federation pattern that combines: +- **Dynamic remote URL resolution** - Change remote URLs at runtime +- **Synchronous import syntax** - Use static imports that feel like local modules +- **Runtime plugins** - Modify remote configurations before module loading +- **Fallback mechanisms** - Handle remote failures gracefully +- **Modern React patterns** - React 18, TypeScript, error boundaries -Run `pnpm start`. This will build and serve both `app1` and `app2` on ports -`3001` and `3002` respectively. +## 🧩 Architecture Overview -- [localhost:3001](http://localhost:3001/) (HOST) -- [localhost:3002](http://localhost:3002/) (STANDALONE REMOTE) - +``` +┌─────────────────┐ Runtime Plugin ┌─────────────────┐ +│ App 1 (Host) │ ◄─────────────────────► │ Dynamic Remote │ +│ │ │ Resolution │ +│ import Widget │ │ │ +│ from 'app2/...' │ │ window.app2Url │ +└─────────────────┘ └─────────────────┘ + │ │ + │ ▼ + │ ┌─────────────────┐ + └─────────────────────────────────► App 2 (Remote) │ + │ │ + │ Exposes Widget │ + └─────────────────┘ +``` -# Running Cypress E2E Tests +## 🚀 Quick Start -To run tests in interactive mode, run `npm run cypress:debug` from the root directory of the project. It will open Cypress Test Runner and allow to run tests in interactive mode. [More info about "How to run tests"](../../cypress-e2e/README.md#how-to-run-tests) +### Prerequisites +- Node.js 16+ +- pnpm (recommended) or npm/yarn -To build app and run test in headless mode, run `yarn e2e:ci`. It will build app and run tests for this workspace in headless mode. If tets failed cypress will create `cypress` directory in sample root folder with screenshots and videos. +### Installation & Running -["Best Practices, Rules amd more interesting information here](../../cypress-e2e/README.md) +```bash +# Install dependencies +pnpm install + +# Start both applications in development mode +pnpm start + +# Or run individually: +pnpm --filter dynamic-remotes-synchronous-imports_app1 start +pnpm --filter dynamic-remotes-synchronous-imports_app2 start +``` + +### Access the Applications +- **Host Application (App 1)**: [http://localhost:3001](http://localhost:3001) +- **Remote Application (App 2)**: [http://localhost:3002](http://localhost:3002) + +## 🔍 What You'll See + +1. **Local Component**: A widget from App 1 with modern React features +2. **Remote Component**: A widget dynamically loaded from App 2 using synchronous import syntax +3. **Shared Dependencies**: Both apps share React and moment.js efficiently +4. **Error Handling**: Comprehensive error boundaries and fallback UI +5. **Debug Information**: Real-time configuration and state monitoring + +## 💡 Why Synchronous Imports with Dynamic Remotes? + +### Traditional Dynamic Imports +```javascript +// ❌ Traditional approach - requires dynamic import syntax +const RemoteComponent = React.lazy(() => import('app2/Widget')); +``` + +### This Example's Approach +```javascript +// ✅ Our approach - looks and feels like a local import +import WidgetRemote from 'app2/Widget'; +``` + +### Key Benefits + +1. **Developer Experience**: + - Familiar import syntax + - Better IDE support and autocomplete + - Easier to refactor and maintain + +2. **Type Safety**: + - Static analysis works correctly + - TypeScript integration is seamless + - Compile-time checks for remote modules + +3. **Performance**: + - No additional lazy loading overhead + - Better tree shaking + - Optimized bundling + +4. **Runtime Flexibility**: + - URL resolution happens at runtime + - Easy environment switching + - Dynamic configuration without code changes + +## 🔧 How It Works: The Runtime Plugin Magic + +### Step 1: Configuration with Placeholders + +```javascript +// moduleConfig.js +const app2Module = { + name: 'app2', + federationConfig: 'app2@[window.app2Url]/remoteEntry.js' + // ^^^^^^^^^^^^^ + // Placeholder for runtime resolution +}; +``` + +### Step 2: Runtime Plugin Resolution + +```javascript +// runtimePlugin.js +const getDynamicRemotePlugin = () => ({ + name: 'dynamic-remote-url-plugin', + beforeRequest: (args) => { + // Look for window.app2Url and replace placeholder + if (window.app2Url) { + remote.entry = window.app2Url + '/remoteEntry.js'; + } + } +}); +``` + +### Step 3: Dynamic URL Setup + +```javascript +// index.js - Set up before app loads +window.app2Url = '//production-server.com/app2'; +// or +window.app2Url = '//localhost:3002'; // development +``` + +### Step 4: Synchronous Import + +```javascript +// App.js - Import as if it's local! +import WidgetRemote from 'app2/Widget'; + +function App() { + return ( +
+ {/* Just works! */} +
+ ); +} +``` + +## 🏗️ Project Structure + +``` +dynamic-remotes-synchronous-imports/ +├── moduleConfig.js # 📋 Centralized configuration +├── app1/ # 🏠 Host application +│ ├── runtimePlugin.js # 🔌 Dynamic URL resolution plugin +│ ├── webpack.config.js # ⚙️ Module Federation config +│ └── src/ +│ ├── App.tsx # 🎯 Main application with remote imports +│ ├── components/ +│ │ └── ErrorBoundary.tsx # 🛡️ Error handling +│ └── types/ +│ └── module-federation.d.ts # 📝 TypeScript definitions +└── app2/ # 🔗 Remote application + ├── webpack.config.js # ⚙️ Exposes Widget component + └── src/ + └── Widget.js # 🧩 Federated component +``` + +## 🎨 Modern Features Demonstrated + +### Module Federation 2.0 Enhancements +- ✅ Enhanced runtime plugins with comprehensive error handling +- ✅ Improved shared dependency management +- ✅ Manifest generation for debugging +- ✅ Hoisted federation runtime +- ✅ Better fallback mechanisms + +### React 18 Features +- ✅ `createRoot` API for better performance +- ✅ Concurrent features support +- ✅ Modern hooks patterns +- ✅ Comprehensive error boundaries + +### TypeScript Integration +- ✅ Type definitions for federated modules +- ✅ Runtime plugin type safety +- ✅ Development-time type checking +- ✅ Autocomplete for remote modules + +### Developer Experience +- ✅ Hot module replacement +- ✅ Debug utilities and logging +- ✅ Configuration validation +- ✅ Real-time monitoring + +## 🌍 Use Cases + +This pattern is perfect when you need: + +### 1. **Multi-Environment Deployments** +```javascript +// Dynamically point to different environments +window.app2Url = process.env.NODE_ENV === 'production' + ? '//cdn.company.com/app2' + : '//staging.company.com/app2'; +``` + +### 2. **A/B Testing** +```javascript +// Route users to different remote versions +window.app2Url = userInTestGroup + ? '//test-remotes.company.com/app2' + : '//stable-remotes.company.com/app2'; +``` + +### 3. **Gradual Rollouts** +```javascript +// Progressive deployment +const rolloutPercentage = getUserRolloutPercentage(); +window.app2Url = rolloutPercentage < 50 + ? '//v1.company.com/app2' + : '//v2.company.com/app2'; +``` + +### 4. **Tenant-Specific Modules** +```javascript +// Multi-tenant applications +window.app2Url = `//tenant-${tenantId}.company.com/app2`; +``` + +## 🔧 Configuration Options + +### Basic Setup +```javascript +// Set primary URL +window.app2Url = '//your-remote-server.com'; +``` + +### With Fallback +```javascript +// Set primary and fallback URLs +window.app2Url = '//primary-server.com'; +window.app2FallbackUrl = '//fallback-server.com'; +``` + +### Programmatic Configuration +```javascript +import { configUtils } from './moduleConfig'; + +// Setup multiple remotes at once +configUtils.setupDynamicUrls({ + app2: '//server1.com', + app3: '//server2.com' +}); + +// Setup fallbacks +configUtils.setupFallbackUrls({ + app2: '//fallback1.com', + app3: '//fallback2.com' +}); +``` + +## 🛠️ Development + +### Building for Production +```bash +# Build all applications +pnpm build + +# Serve built applications +pnpm serve +``` + +### Debugging + +#### Browser Console +```javascript +// Check current configuration +window.__MF_CONFIG__.getCurrentConfig(); + +// Validate setup +window.__MF_CONFIG__.validateConfiguration(); + +// Check plugin state +window.__MF_DEBUG__.getDynamicRemotePluginState(); +``` + +#### Development Tools +- Open browser DevTools +- Check the "Dynamic Remote Plugin" logs +- Monitor network requests for remote modules +- Use the debug panel in the application + +## 🧪 Testing + +### End-to-End Tests +```bash +# Run Cypress tests +pnpm e2e:ci +``` + +### Manual Testing Scenarios + +1. **Normal Operation**: Both apps running, remote loads successfully +2. **Remote Unavailable**: Stop app2, verify error handling +3. **Network Issues**: Use network throttling, test retry logic +4. **URL Changes**: Modify `window.app2Url` in runtime, verify updates + +## 🔍 Troubleshooting + +### Common Issues + +#### Remote Module Fails to Load +``` +❌ Error: Loading script failed +``` + +**Solutions:** +1. Verify remote application is running +2. Check `window.app2Url` is set correctly +3. Ensure CORS headers are configured +4. Verify network connectivity + +#### TypeScript Errors +``` +❌ Cannot find module 'app2/Widget' +``` + +**Solutions:** +1. Ensure type definitions are in place +2. Check `tsconfig.json` includes paths +3. Restart TypeScript server + +#### Shared Dependencies Issues +``` +❌ Multiple React instances detected +``` + +**Solutions:** +1. Verify `singleton: true` in shared config +2. Check version compatibility +3. Ensure both apps use same React version + +### Debug Checklist + +- [ ] Remote application is running and accessible +- [ ] `window.app2Url` is set before application loads +- [ ] CORS headers allow cross-origin requests +- [ ] Shared dependencies are properly configured +- [ ] Network connectivity is working +- [ ] Browser console shows no errors + +## 📚 Additional Resources + +### Official Documentation +- [Module Federation Documentation](https://module-federation.io/) +- [Webpack Module Federation](https://webpack.js.org/concepts/module-federation/) + +### Related Examples +- [Basic Module Federation](../../basic/host-remote/) +- [Advanced Routing](../../advanced-api/automatic-vendor-sharing/) + +### Community +- [Module Federation GitHub](https://github.com/module-federation/core) +- [Discord Community](https://discord.gg/module-federation) + +## 🤝 Contributing + +Found an issue or want to improve this example? + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## 📄 License + +MIT License - see the [LICENSE](../../LICENSE) file for details. + +--- + +**⭐ Key Takeaway**: This example demonstrates that you can have the best of both worlds - the simplicity and developer experience of synchronous imports combined with the flexibility of dynamic remote URL resolution at runtime. diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/package.json b/advanced-api/dynamic-remotes-synchronous-imports/app1/package.json index 77ec3a77036..fb7c5e54389 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/package.json +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/package.json @@ -4,10 +4,14 @@ "devDependencies": { "@babel/core": "7.24.7", "@babel/preset-react": "7.24.7", + "@babel/preset-typescript": "^7.24.7", "@module-federation/enhanced": "0.17.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "babel-loader": "9.1.3", "html-webpack-plugin": "5.6.0", "serve": "14.2.3", + "typescript": "^5.5.3", "webpack": "5.101.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" @@ -20,7 +24,7 @@ }, "dependencies": { "moment": "^2.29.4", - "react": "^16.13.0", - "react-dom": "^16.13.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" } } \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/runtimePlugin.js b/advanced-api/dynamic-remotes-synchronous-imports/app1/runtimePlugin.js index 2fc134f1482..75592c8b337 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/runtimePlugin.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/runtimePlugin.js @@ -1,28 +1,275 @@ -const getRemote = () => { +/** + * Enhanced runtime plugin for dynamic remote URL resolution + * Supports synchronous imports while dynamically changing remote URLs at runtime + * + * This plugin implements Module Federation 2.0 runtime hooks to enable: + * - Dynamic remote URL resolution based on global variables + * - Fallback mechanisms for failed remote loads + * - Comprehensive error handling and logging + * - URL validation and security checks + * + * @returns {import('@module-federation/enhanced/runtime').ModuleFederationRuntimePlugin} + */ +const getDynamicRemotePlugin = () => { + // Plugin state management + const pluginState = { + resolvedUrls: new Map(), + failedRemotes: new Set(), + retryAttempts: new Map(), + maxRetries: 3 + }; + + /** + * Enhanced URL resolution with fallback support + * @param {string} remoteName - Name of the remote + * @param {string} originalEntry - Original entry URL + * @returns {string} Resolved URL + */ + const resolveRemoteUrl = (remoteName, originalEntry) => { + const globalVarName = `${remoteName}Url`; + const fallbackVarName = `${remoteName}FallbackUrl`; + + // Check if URL was already resolved and cached + if (pluginState.resolvedUrls.has(remoteName)) { + return pluginState.resolvedUrls.get(remoteName); + } + + let resolvedUrl = originalEntry; + + // Try primary URL from global variable + if (typeof window !== 'undefined' && window[globalVarName]) { + const placeholderPattern = /\[window\.[^\]]+]/; + const match = originalEntry.match(placeholderPattern); + + if (match) { + const pathAfterPlaceholder = originalEntry.split(placeholderPattern)[1] || ''; + const candidateUrl = window[globalVarName] + pathAfterPlaceholder; + + if (isValidRemoteUrl(candidateUrl)) { + resolvedUrl = candidateUrl; + console.log(`[Dynamic Remote Plugin] Resolved URL for '${remoteName}':`, { + source: 'primary', + url: resolvedUrl + }); + } + } + } + + // Try fallback URL if primary failed or unavailable + if (resolvedUrl === originalEntry && typeof window !== 'undefined' && window[fallbackVarName]) { + const fallbackUrl = window[fallbackVarName]; + if (isValidRemoteUrl(fallbackUrl)) { + resolvedUrl = fallbackUrl; + console.log(`[Dynamic Remote Plugin] Using fallback URL for '${remoteName}':`, resolvedUrl); + } + } + + // Cache the resolved URL + pluginState.resolvedUrls.set(remoteName, resolvedUrl); + return resolvedUrl; + }; + return { - name: 'get-remote-from-window-plugin', - beforeRequest: args => { - // The 'beforeRequest' hook is called before a request is made. - // It iterates over each 'remote' in 'args.options.remotes'. - args.options.remotes.forEach(remote => { - // Constructing the reference name from the remote's name. - const refName = `${remote.name}Url`; - console.log(refName); - // Checking if the global variable corresponding to 'refName' exists. - if (typeof window[refName] !== 'undefined') { - // Splitting the 'remote.entry' string on the pattern '[window.anyVariableName]' - // and taking the part after this pattern. - const split = remote.entry.split(/\[window\.[^\]]+]/)[1]; - // If there's a string after the split (i.e., the pattern was found), - // prepend the value of the global variable to this string. - if (split) { - remote.entry = window[refName] + split; + name: 'enhanced-dynamic-remote-plugin', + version: '2.0.0', + + /** + * init hook - called when the plugin is initialized + */ + init: (args) => { + console.log('[Dynamic Remote Plugin] Initializing enhanced dynamic remote plugin v2.0.0'); + + // Setup global error handlers for remote loading + if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', (event) => { + if (event.reason?.message?.includes('Loading script failed')) { + console.error('[Dynamic Remote Plugin] Remote script loading failed:', event.reason); } + }); + } + + return args; + }, + + /** + * beforeRequest hook - called before resolving remote modules + */ + beforeRequest: (args) => { + try { + console.log('[Dynamic Remote Plugin] Processing request:', { + id: args.id, + remotesCount: args.options?.remotes?.length || 0 + }); + + // Validate args structure + if (!args.options || !Array.isArray(args.options.remotes)) { + console.warn('[Dynamic Remote Plugin] Invalid args structure, skipping processing'); + return args; } + + // Process each remote configuration + args.options.remotes.forEach((remote, index) => { + if (!remote || !remote.name || !remote.entry) { + console.warn(`[Dynamic Remote Plugin] Invalid remote configuration at index ${index}:`, remote); + return; + } + + // Skip if this remote has failed too many times + const retryCount = pluginState.retryAttempts.get(remote.name) || 0; + if (retryCount >= pluginState.maxRetries) { + console.warn(`[Dynamic Remote Plugin] Skipping remote '${remote.name}' - max retries exceeded`); + return; + } + + // Resolve the remote URL with fallback support + const resolvedUrl = resolveRemoteUrl(remote.name, remote.entry); + + if (resolvedUrl !== remote.entry) { + console.log(`[Dynamic Remote Plugin] URL resolved for '${remote.name}':`, { + original: remote.entry, + resolved: resolvedUrl + }); + remote.entry = resolvedUrl; + } + }); + + return args; + } catch (error) { + console.error('[Dynamic Remote Plugin] Error in beforeRequest hook:', error); + // Return original args to prevent breaking the application + return args; + } + }, + + /** + * Enhanced error handling for remote loading failures + */ + errorLoadRemote: (args) => { + const { id, error, origin } = args; + const remoteName = id?.split('/')[0]; + + if (remoteName) { + // Track retry attempts + const currentRetries = pluginState.retryAttempts.get(remoteName) || 0; + pluginState.retryAttempts.set(remoteName, currentRetries + 1); + + // Mark as failed if max retries exceeded + if (currentRetries >= pluginState.maxRetries) { + pluginState.failedRemotes.add(remoteName); + console.error(`[Dynamic Remote Plugin] Remote '${remoteName}' marked as failed after ${pluginState.maxRetries} attempts`); + } + } + + console.error('[Dynamic Remote Plugin] Failed to load remote:', { + id, + remoteName, + error: error?.message || error, + origin, + retryAttempt: pluginState.retryAttempts.get(remoteName) || 0 }); + + return args; + }, + + /** + * afterResolve hook - called after a remote is successfully resolved + */ + afterResolve: (args) => { + const remoteName = args.id?.split('/')[0]; + if (remoteName && pluginState.failedRemotes.has(remoteName)) { + // Remote recovered, remove from failed list + pluginState.failedRemotes.delete(remoteName); + pluginState.retryAttempts.delete(remoteName); + console.log(`[Dynamic Remote Plugin] Remote '${remoteName}' recovered successfully`); + } return args; }, + + /** + * Expose plugin state for debugging + */ + getPluginState: () => ({ + resolvedUrls: Object.fromEntries(pluginState.resolvedUrls), + failedRemotes: Array.from(pluginState.failedRemotes), + retryAttempts: Object.fromEntries(pluginState.retryAttempts) + }) }; }; -export default getRemote; +/** + * Enhanced URL validation for Module Federation remotes + * @param {string} url - URL to validate + * @returns {boolean} True if valid + */ +function isValidRemoteUrl(url) { + try { + if (!url || typeof url !== 'string') { + return false; + } + + // Remove whitespace + url = url.trim(); + + // Check for basic URL structure + const hasValidProtocol = url.startsWith('http://') || + url.startsWith('https://') || + url.startsWith('//') || + url.startsWith('/'); + + if (!hasValidProtocol) { + return false; + } + + // Must end with .js for Module Federation + if (!url.endsWith('.js')) { + return false; + } + + // Security check: prevent javascript: URLs and other dangerous schemes + if (url.toLowerCase().includes('javascript:') || + url.toLowerCase().includes('data:') || + url.toLowerCase().includes('vbscript:')) { + return false; + } + + // For full URLs, validate with URL constructor + if (url.startsWith('http')) { + try { + new URL(url); + } catch { + return false; + } + } + + return true; + } catch { + return false; + } +} + +/** + * Utility function to expose plugin debugging information globally + */ +if (typeof window !== 'undefined') { + window.__MF_DEBUG__ = { + getDynamicRemotePluginState: () => { + // This will be set by the plugin instance + return window.__MF_PLUGIN_STATE__ || null; + } + }; +} + +// CommonJS export for webpack runtime plugin compatibility +module.exports = getDynamicRemotePlugin; + +// Also expose as named export for modern import syntax +module.exports.getDynamicRemotePlugin = getDynamicRemotePlugin; +module.exports.isValidRemoteUrl = isValidRemoteUrl; + +// Set up global debugging state if in browser +if (typeof window !== 'undefined') { + const pluginInstance = getDynamicRemotePlugin(); + if (pluginInstance.getPluginState) { + window.__MF_PLUGIN_STATE__ = pluginInstance.getPluginState; + } +} diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/App.js b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/App.js deleted file mode 100644 index 8afbf9b3c3c..00000000000 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/App.js +++ /dev/null @@ -1,16 +0,0 @@ -import Widget from './Widget'; -import React, { Suspense } from 'react'; -import WidgetRemote from 'app2/Widget'; - -const App = () => ( -
-

Dynamic System Host

-

App 1

- - - - -
-); - -export default App; diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/App.tsx b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/App.tsx new file mode 100644 index 00000000000..f2e7c3a7269 --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/App.tsx @@ -0,0 +1,263 @@ +import React, { Suspense, useState, useEffect, useCallback } from 'react'; +import Widget from './Widget'; +import ErrorBoundary from './components/ErrorBoundary'; +import { configUtils } from '../../moduleConfig'; + +// Import remote component with synchronous syntax +// This demonstrates the core pattern of dynamic remotes with synchronous imports +import WidgetRemote from 'app2/Widget'; + +/** + * Enhanced App Component with Module Federation 2.0 patterns + * + * Demonstrates: + * - Synchronous imports of dynamic remotes + * - Error boundaries for remote module failures + * - Loading states and fallbacks + * - Runtime configuration debugging + * - Modern React patterns (hooks, functional components) + */ +const App: React.FC = () => { + const [remoteConfig, setRemoteConfig] = useState(null); + const [remoteError, setRemoteError] = useState(null); + const [retryKey, setRetryKey] = useState(0); + + /** + * Load and validate Module Federation configuration + */ + useEffect(() => { + const config = configUtils.getCurrentConfig(); + const validation = configUtils.validateConfiguration(); + + setRemoteConfig(config); + + if (!validation.isValid) { + console.warn('[App] Configuration issues detected:', validation.issues); + setRemoteError(validation.issues.join(', ')); + } + + // Log configuration for debugging + console.log('[App] Module Federation configuration:', config); + }, []); + + /** + * Handle errors from remote components + */ + const handleRemoteError = useCallback((error: Error, errorInfo: any) => { + console.error('[App] Remote component error:', error); + setRemoteError(error.message); + + // Optionally attempt to reconfigure remotes + if (error.message.includes('Loading')) { + console.log('[App] Attempting to reconfigure remote URLs...'); + // You could implement auto-recovery logic here + } + }, []); + + /** + * Retry loading remote components + */ + const handleRetry = useCallback(() => { + setRemoteError(null); + setRetryKey(prev => prev + 1); + console.log('[App] Retrying remote component load...'); + }, []); + + /** + * Loading fallback for remote components + */ + const RemoteLoadingFallback = () => ( +
+
🔄
+
Loading Remote Component...
+
+ Fetching app2/Widget from remote module +
+
+ ); + + /** + * Debug panel for development + */ + const DebugPanel = () => { + if (process.env.NODE_ENV !== 'development') return null; + + return ( +
+ + 🔧 Module Federation Debug Info + + + {remoteConfig && ( +
+

Configuration:

+
+              {JSON.stringify(remoteConfig, null, 2)}
+            
+
+ )} + + {remoteError && ( +
+

Configuration Issues:

+

{remoteError}

+ +
+ )} + +
+

Global Debug Access:

+

Open browser console and try:

+ + window.__MF_CONFIG__.getCurrentConfig() + +
+
+ ); + }; + + return ( +
+
+

+ 🌐 Dynamic System Host +

+

+ App 1 - Demonstrating Synchronous Imports with Dynamic Remotes +

+
+ +
+ {/* Local Component */} +
+

📍 Local Component

+
+ +
+
+ + {/* Remote Component with Enhanced Error Handling */} +
+

🔌 Remote Component (Synchronous Import)

+
+ +
⚠️
+
Remote Module Failed to Load
+
+ The remote component 'app2/Widget' could not be loaded. +
+ +
+ } + > + }> + + + +
+ + + {/* Debug Panel */} + +
+ +
+

+ This example demonstrates synchronous imports with dynamic remote URLs. +
+ The remote URL is resolved at runtime while maintaining the synchronous import syntax. +

+
+
+ ); +}; + +export default App; diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/Widget.js b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/Widget.js index a0d7816dd3a..3fd59ffb572 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/Widget.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/Widget.js @@ -1,22 +1,144 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import moment from 'moment'; +/** + * Enhanced local Widget component for App 1 + * + * Demonstrates: + * - Modern React functional component with hooks + * - Shared dependency usage (moment.js) + * - State management + * - Effect handling + * - Accessibility features + * - Modern styling patterns + */ export default function Widget() { + const [loadTime] = useState(() => new Date().toLocaleTimeString()); + const [isVisible, setIsVisible] = useState(false); + const [clickCount, setClickCount] = useState(0); + const [currentTime, setCurrentTime] = useState(() => moment()); + + useEffect(() => { + // Animate in after mount + const timer = setTimeout(() => setIsVisible(true), 100); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + // Update time every second + const interval = setInterval(() => { + setCurrentTime(moment()); + }, 1000); + + return () => clearInterval(interval); + }, []); + + const handleInteraction = () => { + setClickCount(prev => prev + 1); + }; + return (
e.key === 'Enter' && handleInteraction()} + tabIndex={0} + role="button" + aria-label="Local widget from App 1" > -

App 1 Widget

-

- Moment shouldn't download twice, the host has no moment.js
{' '} - {moment().format('MMMM Do YYYY, h:mm:ss a')} +

+

+ 🏠 App 1 Local Widget +

+ +
+ Loaded: {loadTime} +
+
+ +

+ This is a local component that demonstrates shared dependency usage. + Moment.js is shared between the host and remote applications.

+ +
+
+ 📅 Live Time (via shared moment.js): +
+
+ {currentTime.format('MMMM Do YYYY, h:mm:ss a')} +
+
+ +
+ + 🖱️ Interactions: {clickCount} + + + + LOCAL + +
+ + {clickCount > 0 && ( +
+ 🎉 Thank you for interacting! This demonstrates stateful local components. +
+ )}
); } diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/bootstrap.js b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/bootstrap.js index a8680f71cdf..1fcdaded60e 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/bootstrap.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/bootstrap.js @@ -1,5 +1,8 @@ -import App from './App'; +import App from './App.tsx'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -ReactDOM.render(, document.getElementById('root')); +// Using React 18 createRoot API for better performance and concurrent features +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/components/ErrorBoundary.tsx b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000000..b00aeffcc82 --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/components/ErrorBoundary.tsx @@ -0,0 +1,234 @@ +import React, { Component, ReactNode, ErrorInfo } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; + onError?: (error: Error, errorInfo: ErrorInfo) => void; + resetOnPropsChange?: boolean; + resetKeys?: Array; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; + prevResetKeys: Array; +} + +/** + * Enhanced Error Boundary for Module Federation + * + * Provides robust error handling for: + * - Remote module loading failures + * - Runtime errors in federated components + * - Network issues during dynamic imports + * - Graceful fallback UI rendering + */ +export class ErrorBoundary extends Component { + private resetTimeoutId: number | null = null; + + constructor(props: Props) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + prevResetKeys: props.resetKeys || [] + }; + } + + static getDerivedStateFromError(error: Error): Partial { + // Update state so the next render will show the fallback UI + return { + hasError: true, + error + }; + } + + static getDerivedStateFromProps(props: Props, state: State): Partial | null { + const { resetKeys = [] } = props; + const { prevResetKeys } = state; + + // Reset error state if resetKeys have changed + if (state.hasError && resetKeys.some((key, idx) => key !== prevResetKeys[idx])) { + return { + hasError: false, + error: null, + errorInfo: null, + prevResetKeys: resetKeys + }; + } + + return { prevResetKeys: resetKeys }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('[ErrorBoundary] Caught an error:', error); + console.error('[ErrorBoundary] Error info:', errorInfo); + + this.setState({ + error, + errorInfo + }); + + // Call optional error handler + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + + // Log to external service if in production + if (process.env.NODE_ENV === 'production') { + this.logErrorToService(error, errorInfo); + } + } + + componentWillUnmount() { + if (this.resetTimeoutId) { + clearTimeout(this.resetTimeoutId); + } + } + + private logErrorToService(error: Error, errorInfo: ErrorInfo) { + // Implementation for logging to external error tracking service + // This could be Sentry, LogRocket, Bugsnag, etc. + console.log('[ErrorBoundary] Would log to error service:', { error, errorInfo }); + } + + private handleRetry = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null + }); + }; + + private handleAutoRetry = () => { + this.resetTimeoutId = window.setTimeout(() => { + // Check if component is still mounted before retrying + if (this.state.hasError) { + console.log('[ErrorBoundary] Auto-retrying after error...'); + this.handleRetry(); + } + }, 5000); + }; + + private renderDefaultFallback() { + const { error, errorInfo } = this.state; + const isModuleFederationError = error?.message?.includes('Loading') || + error?.message?.includes('remote') || + error?.name === 'ChunkLoadError'; + + return ( +
+

+ {isModuleFederationError ? + '🔌 Remote Module Loading Error' : + '⚠️ Something went wrong' + } +

+ +

+ {isModuleFederationError ? ( + <>This usually happens when a remote module is unavailable or the URL is incorrect. + ) : ( + <>An unexpected error occurred while rendering this component. + )} +

+ +
+ + + {isModuleFederationError && ( + + )} +
+ + {process.env.NODE_ENV === 'development' && ( +
+ + 🐛 Debug Information + +
+              Error: {error?.toString()}
+              {errorInfo?.componentStack && (
+                <>
+                  {'\n\n'}Component Stack:{errorInfo.componentStack}
+                
+              )}
+            
+
+ )} +
+ ); + } + + render() { + if (this.state.hasError) { + // Render custom fallback UI or default + return this.props.fallback || this.renderDefaultFallback(); + } + + return this.props.children; + } +} + +/** + * Hook version of Error Boundary for functional components + * Note: This is a wrapper that uses the class-based ErrorBoundary + */ +export function withErrorBoundary

( + Component: React.ComponentType

, + errorBoundaryProps?: Omit +) { + const WrappedComponent = (props: P) => ( + + + + ); + + WrappedComponent.displayName = `withErrorBoundary(${Component.displayName || Component.name})`; + + return WrappedComponent; +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/index.js b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/index.js index 51623bd18f0..59ff5d5e0d1 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/index.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/index.js @@ -1,5 +1,38 @@ +/** + * Dynamic Remote URL Initialization + * + * This file demonstrates how to set up global variables that the runtime plugin + * will use to dynamically resolve remote URLs while maintaining synchronous imports. + */ + import { app2Module } from '../../moduleConfig'; +// Set up the global variable that the runtime plugin will check +// This allows dynamic URL resolution at runtime window[app2Module.urlGlobalVariable] = app2Module.url; -import('./bootstrap'); +console.log(`[Dynamic Remote Init] Setting ${app2Module.urlGlobalVariable} to:`, app2Module.url); + +// Example: You could dynamically change this based on environment or user settings +// window.app2Url = process.env.REACT_APP_REMOTE_URL || app2Module.url; + +// You could also set multiple remotes dynamically: +// window.app3Url = '//different-host:4001'; +// window.app4Url = '//cdn.example.com/remotes'; + +// Bootstrap the application after setting up dynamic configurations +import('./bootstrap').catch(error => { + console.error('[Dynamic Remote Init] Failed to bootstrap application:', error); + + // Optionally show user-friendly error message + document.body.innerHTML = ` +

+

Application Failed to Load

+

Unable to initialize the application. Please check the console for details.

+
+ Error Details +
${error.message}
+
+
+ `; +}); diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/src/types/module-federation.d.ts b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/types/module-federation.d.ts new file mode 100644 index 00000000000..7c3ce31d53b --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/src/types/module-federation.d.ts @@ -0,0 +1,86 @@ +/** + * Type definitions for Module Federation dynamic remotes + */ + +declare module 'app2/Widget' { + import { ComponentType } from 'react'; + const WidgetRemote: ComponentType; + export default WidgetRemote; +} + +// Global window extensions for dynamic remote URL configuration +declare global { + interface Window { + /** + * Global variable for app2 remote URL + * Set this before the application loads to dynamically configure the remote URL + */ + app2Url?: string; + + /** + * Fallback URL for app2 remote + * Used when the primary URL fails to load + */ + app2FallbackUrl?: string; + + /** + * Module Federation debugging utilities + */ + __MF_DEBUG__?: { + getDynamicRemotePluginState?: () => { + resolvedUrls: Record; + failedRemotes: string[]; + retryAttempts: Record; + } | null; + }; + + /** + * Internal Module Federation plugin state (for debugging) + */ + __MF_PLUGIN_STATE__?: () => { + resolvedUrls: Record; + failedRemotes: string[]; + retryAttempts: Record; + }; + } +} + +/** + * Module Federation runtime plugin types + */ +export interface ModuleFederationRuntimePlugin { + name: string; + version?: string; + init?: (args: any) => any; + beforeRequest?: (args: BeforeRequestArgs) => BeforeRequestArgs; + afterResolve?: (args: any) => any; + errorLoadRemote?: (args: ErrorLoadRemoteArgs) => any; + getPluginState?: () => PluginState; +} + +export interface BeforeRequestArgs { + id: string; + options: { + remotes: RemoteConfig[]; + }; +} + +export interface RemoteConfig { + name: string; + entry: string; + [key: string]: any; +} + +export interface ErrorLoadRemoteArgs { + id: string; + error: Error | string; + origin?: string; +} + +export interface PluginState { + resolvedUrls: Record; + failedRemotes: string[]; + retryAttempts: Record; +} + +export {}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/tsconfig.json b/advanced-api/dynamic-remotes-synchronous-imports/app1/tsconfig.json new file mode 100644 index 00000000000..209cb11c2b6 --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src/**/*", + "runtimePlugin.js", + "../moduleConfig.js" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app1/webpack.config.js b/advanced-api/dynamic-remotes-synchronous-imports/app1/webpack.config.js index 4301b41c248..8cd1feb2475 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app1/webpack.config.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app1/webpack.config.js @@ -1,35 +1,30 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('@module-federation/enhanced'); -const path = require('path'); -const deps = require('./package.json').dependencies; const { app2Module, app1Module } = require('../moduleConfig'); +const deps = require('./package.json').dependencies; module.exports = { - entry: ['./src/index'], + entry: './src/index', mode: 'development', - target: 'web', devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, + port: app1Module.port, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, - port: app1Module.port, }, - output: { - publicPath: 'auto', + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], }, module: { rules: [ { - test: /\.jsx?$/, + test: /\.(js|jsx|ts|tsx)$/, loader: 'babel-loader', exclude: /node_modules/, options: { - presets: ['@babel/preset-react'], + presets: ['@babel/preset-react', '@babel/preset-typescript'], }, }, ], @@ -39,7 +34,7 @@ module.exports = { name: app1Module.name, filename: app1Module.fileName, remotes: { - app2: app2Module.federationConfig, + app2: 'app2@//localhost:3002/remoteEntry.js', }, runtimePlugins: [require.resolve('./runtimePlugin.js')], shared: { @@ -61,4 +56,4 @@ module.exports = { template: './public/index.html', }), ], -}; +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app2/package.json b/advanced-api/dynamic-remotes-synchronous-imports/app2/package.json index e4de469391f..434327fb8c3 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app2/package.json +++ b/advanced-api/dynamic-remotes-synchronous-imports/app2/package.json @@ -4,10 +4,14 @@ "devDependencies": { "@babel/core": "7.24.7", "@babel/preset-react": "7.24.7", + "@babel/preset-typescript": "^7.24.7", "@module-federation/enhanced": "0.17.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "babel-loader": "9.1.3", "html-webpack-plugin": "5.6.0", "serve": "14.2.3", + "typescript": "^5.5.3", "webpack": "5.101.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" @@ -20,8 +24,8 @@ }, "dependencies": { "moment": "^2.29.4", - "react": "^16.13.0", - "react-dom": "^16.13.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-redux": "^7.2.0", "redux": "^4.2.1" } diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app2/src/App.js b/advanced-api/dynamic-remotes-synchronous-imports/app2/src/App.js index 940f914e63c..68b916ef536 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app2/src/App.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app2/src/App.js @@ -3,7 +3,7 @@ import React from 'react'; const App = () => (
-

Dynamic System Host

+

🌐 Dynamic System Host

App 2

diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app2/src/Widget.js b/advanced-api/dynamic-remotes-synchronous-imports/app2/src/Widget.js index b6186724b42..2e55a645e66 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app2/src/Widget.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app2/src/Widget.js @@ -1,22 +1,232 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import moment from 'moment'; +/** + * Enhanced remote Widget component for App 2 + * + * This component is exposed via Module Federation and consumed by other applications. + * + * Demonstrates: + * - Remote component with modern React patterns + * - Shared dependency usage (moment.js) + * - State management and effects in federated components + * - Performance optimizations + * - Accessibility features + * - Error resilience + */ export default function Widget() { + const [loadTime] = useState(() => new Date().toLocaleTimeString()); + const [isVisible, setIsVisible] = useState(false); + const [clickCount, setClickCount] = useState(0); + const [currentTime, setCurrentTime] = useState(() => moment()); + const [isRunning, setIsRunning] = useState(true); + const intervalRef = useRef(); + + useEffect(() => { + // Animate in after mount + const timer = setTimeout(() => setIsVisible(true), 200); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + // Update time every second when running + if (isRunning) { + intervalRef.current = setInterval(() => { + setCurrentTime(moment()); + }, 1000); + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isRunning]); + + const handleInteraction = () => { + setClickCount(prev => prev + 1); + }; + + const toggleTimer = () => { + setIsRunning(prev => !prev); + }; + + const formatTimeZone = () => { + return moment().format('Z'); + }; + return (
e.key === 'Enter' && handleInteraction()} + tabIndex={0} + role="button" + aria-label="Remote widget from App 2" > -

App 2 Widget

-

- Using momentjs for format the date -

-

{moment().format('MMMM Do YYYY, h:mm:ss a')}

+ {/* Animated background effect */} +
+ +
+
+

+ 🔌 App 2 Remote Widget +

+ +
+ Loaded: {loadTime} +
+
+ +

+ This is a remote component from App 2, federated via Module Federation. + It demonstrates dynamic loading with synchronous import syntax. +

+ +
+
+
+ 📅 Live Time (via shared moment.js): +
+ +
+ +
+ {currentTime.format('MMMM Do YYYY, h:mm:ss a')} +
+ +
+ Timezone: {formatTimeZone()} +
+
+ +
+ + 🖱️ Interactions: {clickCount} + + +
+ + {isRunning ? '🟢 LIVE' : '🔴 PAUSED'} + + + + REMOTE + +
+
+ + {clickCount > 0 && ( +
+ 🎉 Remote component interaction successful! + This proves the federated module is working correctly. +
+ )} +
); } diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app2/src/bootstrap.js b/advanced-api/dynamic-remotes-synchronous-imports/app2/src/bootstrap.js index a8680f71cdf..a68d81c41fd 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app2/src/bootstrap.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app2/src/bootstrap.js @@ -1,5 +1,8 @@ import App from './App'; import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; -ReactDOM.render(, document.getElementById('root')); +// Using React 18 createRoot API for better performance and concurrent features +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app2/tsconfig.json b/advanced-api/dynamic-remotes-synchronous-imports/app2/tsconfig.json new file mode 100644 index 00000000000..067ccdcef2a --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/app2/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src/**/*", + "../moduleConfig.js" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/app2/webpack.config.js b/advanced-api/dynamic-remotes-synchronous-imports/app2/webpack.config.js index 839012280dd..32cd524a812 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/app2/webpack.config.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/app2/webpack.config.js @@ -1,6 +1,5 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin; -const path = require('path'); +const { ModuleFederationPlugin } = require('@module-federation/enhanced'); const { app2Module } = require('../moduleConfig'); const deps = require('./package.json').dependencies; @@ -8,19 +7,12 @@ module.exports = { entry: './src/index', mode: 'development', devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, + port: app2Module.port, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', }, - port: app2Module.port, - }, - target: 'web', - output: { - publicPath: 'auto', }, module: { rules: [ @@ -42,10 +34,6 @@ module.exports = { exposes: { './Widget': './src/Widget', }, - // adds react as shared module - // version is inferred from package.json - // there is no version check for the required version - // so it will always use the higher version found shared: { react: { requiredVersion: deps.react, @@ -68,4 +56,4 @@ module.exports = { template: './public/index.html', }), ], -}; +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/e2e/checkDynamicRemotesSynchImportApps.spec.ts b/advanced-api/dynamic-remotes-synchronous-imports/e2e/checkDynamicRemotesSynchImportApps.spec.ts new file mode 100644 index 00000000000..db7ca0328e8 --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/e2e/checkDynamicRemotesSynchImportApps.spec.ts @@ -0,0 +1,325 @@ +import { test, expect, Page } from '@playwright/test'; + +// Helper functions +async function openLocalhost(page: Page, port: number) { + await page.goto(`http://localhost:${port}`); + await page.waitForLoadState('networkidle'); +} + +async function checkElementWithTextPresence(page: Page, selector: string, text: string, timeout: number = 10000) { + // Use getByText for exact text matching to avoid conflicts with partial matches + await page.locator(selector).filter({ hasText: new RegExp(`^${text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) }).first().waitFor({ timeout }); +} + +async function checkElementVisibility(page: Page, selector: string, timeout: number = 10000) { + await page.locator(selector).waitFor({ state: 'visible', timeout }); +} + +async function checkElementBackgroundColor(page: Page, selector: string, expectedColor: string) { + const element = page.locator(selector); + await element.waitFor({ state: 'visible' }); + const backgroundColor = await element.evaluate((el) => { + return window.getComputedStyle(el).backgroundColor; + }); + if (backgroundColor !== expectedColor) { + throw new Error(`Expected background color ${expectedColor}, but got ${backgroundColor}`); + } +} + +async function clickElementWithText(page: Page, selector: string, text: string) { + await page.locator(selector).filter({ hasText: text }).click(); +} + +async function waitForDynamicImport(page: Page, timeout: number = 5000) { + // Wait for any dynamic imports to complete + await page.waitForTimeout(1000); + await page.waitForLoadState('networkidle', { timeout }); +} + +async function checkDateFormat(page: Page) { + // Check for moment.js formatted date (MMMM Do YYYY, h:mm format) + const dateElement = page.locator('text=/[A-Z][a-z]+ \\\\d{1,2}[a-z]{2} \\\\d{4}, \\\\d{1,2}:\\\\d{2}/'); + await dateElement.waitFor({ timeout: 5000 }); +} + +const appsData = [ + { + headerSelector: 'h1', + subHeaderSelector: 'h2', + headerText: '🌐 Dynamic System Host', + appNameText: 'App 1 - Demonstrating Synchronous Imports with Dynamic Remotes', + widgetName: ['App 1 Widget', 'App 2 Widget'], + widgetParagraph: ["Moment shouldn't download twice", "Moment shouldn't download twice"], + widgetColor: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + widgetIndexNumber: 1, + isTwoWidgets: true, + host: 3001, + }, + { + headerSelector: 'h1', + subHeaderSelector: 'h2', + headerText: '🌐 Dynamic System Host', + appNameText: 'App 2', + widgetName: ['App 1 Widget', 'App 2 Widget'], + widgetParagraph: ["Moment shouldn't download twice", "Moment shouldn't download twice"], + widgetColor: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + widgetIndexNumber: 2, + isTwoWidgets: false, + host: 3002, + }, +]; + +test.describe('Dynamic Remotes Synchronous Imports E2E Tests', () => { + + appsData.forEach((appData) => { + const { host, appNameText, headerText, widgetName, widgetParagraph, widgetColor, widgetIndexNumber, isTwoWidgets } = appData; + + test.describe(`Check ${appNameText}`, () => { + test(`should display ${appNameText} elements correctly`, async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + await openLocalhost(page, host); + + // Check header and subheader exist + await checkElementWithTextPresence( + page, + appData.headerSelector, + headerText + ); + await checkElementWithTextPresence( + page, + appData.subHeaderSelector, + appNameText + ); + + // Verify no critical console errors + const criticalErrors = consoleErrors.filter(error => + error.includes('Failed to fetch') || + error.includes('ChunkLoadError') || + error.includes('Module not found') || + (error.includes('TypeError') && !error.includes('DevTools')) + ); + expect(criticalErrors).toHaveLength(0); + }); + + test(`should display widgets correctly in ${appNameText}`, async ({ page }) => { + await openLocalhost(page, host); + + if (isTwoWidgets) { + // App 1 has two widgets (local + remote) + for (let i = 0; i < widgetName.length; i++) { + const widgetSelector = i === 0 ? '[data-e2e="WIDGET__1"]' : '[data-e2e="WIDGET__2"]'; + + // Check widget visibility + await checkElementVisibility(page, widgetSelector); + + // Check widget title + await checkElementWithTextPresence( + page, + appData.subHeaderSelector, + widgetName[i] + ); + + // Check widget paragraph text + await checkElementWithTextPresence( + page, + 'p', + widgetParagraph[i] + ); + + // Check moment.js date formatting + await checkDateFormat(page); + + // Check widget background color + await checkElementBackgroundColor(page, widgetSelector, widgetColor[i]); + } + } else { + // App 2 has one widget + const widgetSelector = '[data-e2e="WIDGET__2"]'; + + // Check widget visibility + await checkElementVisibility(page, widgetSelector); + + // Check widget title + await checkElementWithTextPresence( + page, + appData.subHeaderSelector, + widgetName[widgetIndexNumber - 1] + ); + + // Check widget paragraph text + await checkElementWithTextPresence( + page, + 'p', + widgetParagraph[widgetIndexNumber - 1] + ); + + // Check moment.js date formatting + await checkDateFormat(page); + + // Check widget background color + await checkElementBackgroundColor(page, widgetSelector, widgetColor[1]); + } + }); + }); + }); + + test.describe('Synchronous Import Pattern Tests', () => { + test('should demonstrate synchronous imports with dynamic URL resolution', async ({ page }) => { + const consoleMessages: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'log' && msg.text().includes('get-remote-from-window-plugin')) { + consoleMessages.push(msg.text()); + } + }); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Should have runtime plugin logs indicating dynamic URL resolution + const runtimePluginLogs = consoleMessages.filter(msg => + msg.includes('app2Url') || msg.includes('get-remote-from-window-plugin') + ); + + // Verify runtime plugin is working for dynamic URL resolution + expect(runtimePluginLogs.length).toBeGreaterThan(0); + }); + + test('should load remote modules synchronously', async ({ page }) => { + const networkRequests: string[] = []; + + page.on('request', (request) => { + networkRequests.push(request.url()); + }); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Check that App 2 widget is loaded synchronously (no dynamic import button clicks) + await page.waitForSelector('[data-e2e="WIDGET__2"]', { timeout: 10000 }); + + // Verify the remote entry was loaded + const remoteEntryRequests = networkRequests.filter(url => + url.includes('localhost:3002') && url.includes('remoteEntry.js') + ); + + expect(remoteEntryRequests.length).toBeGreaterThan(0); + }); + + test('should handle runtime URL modification correctly', async ({ page }) => { + // Monitor for URL resolution in runtime plugin + const consoleMessages: string[] = []; + page.on('console', (msg) => { + consoleMessages.push(msg.text()); + }); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Verify both widgets are present (demonstrating successful remote loading) + await page.waitForSelector('[data-e2e="WIDGET__1"]'); + await page.waitForSelector('[data-e2e="WIDGET__2"]'); + + // Check that runtime plugin logged URL processing + const urlResolutionLogs = consoleMessages.filter(msg => + msg.includes('app2Url') || msg.includes('beforeRequest') + ); + + expect(urlResolutionLogs.length).toBeGreaterThan(0); + }); + }); + + test.describe('Module Federation Features', () => { + test('should efficiently share dependencies between applications', async ({ page }) => { + const networkRequests: string[] = []; + + page.on('request', (request) => { + networkRequests.push(request.url()); + }); + + // Navigate to host (loads both local and remote widgets synchronously) + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Navigate to remote standalone + await page.goto('http://localhost:3002'); + await page.waitForLoadState('networkidle'); + + // Verify React is shared efficiently + const reactRequests = networkRequests.filter(url => + url.includes('react') && !url.includes('react-dom') + ); + expect(reactRequests.length).toBeLessThan(8); + + // Verify moment.js is shared between remotes + const momentRequests = networkRequests.filter(url => url.includes('moment')); + expect(momentRequests.length).toBeLessThan(5); + }); + + test('should handle CORS correctly for federated modules', async ({ page }) => { + const corsErrors: string[] = []; + page.on('response', (response) => { + if (response.status() >= 400 && response.url().includes('localhost:300')) { + corsErrors.push(`${response.status()} - ${response.url()}`); + } + }); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Should have no CORS errors + expect(corsErrors).toHaveLength(0); + }); + + test('should demonstrate moment.js sharing', async ({ page }) => { + await openLocalhost(page, 3001); + + // Check that moment.js date is formatted correctly in both widgets + const dateElements = page.locator('text=/[A-Z][a-z]+ \\d{1,2}[a-z]{2} \\d{4}, \\d{1,2}:\\d{2}/'); + + // Should have date formatting in both local and remote widgets + await expect(dateElements).toHaveCount(2); + }); + }); + + test.describe('Error Handling and Resilience', () => { + test('should handle missing window variables gracefully', async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Should handle any missing window variables gracefully + const criticalErrors = consoleErrors.filter(error => + error.includes('Uncaught') && + !error.includes('webpack-dev-server') && + !error.includes('DevTools') && + !error.includes('Warning:') + ); + expect(criticalErrors).toHaveLength(0); + }); + + test('should maintain application stability during remote loading', async ({ page }) => { + await page.goto('http://localhost:3001'); + await page.waitForLoadState('networkidle'); + + // Verify main application elements remain stable + await page.waitForSelector('h1:has-text("Dynamic System Host")'); + await page.waitForSelector('h2:has-text("App 1")'); + + // Verify both widgets loaded successfully + await page.waitForSelector('[data-e2e="WIDGET__1"]'); + await page.waitForSelector('[data-e2e="WIDGET__2"]'); + }); + }); +}); \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/base-test.ts b/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/base-test.ts new file mode 100644 index 00000000000..2ef9807d630 --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/base-test.ts @@ -0,0 +1,65 @@ +import { Page } from '@playwright/test'; + +export class BasePage { + constructor(public page: Page) {} + + async openLocalhost(port: number) { + await this.page.goto(`http://localhost:${port}`); + await this.page.waitForLoadState('networkidle'); + } + + async checkElementWithTextPresence(selector: string, text: string, timeout: number = 10000) { + await this.page.locator(selector).filter({ hasText: text }).waitFor({ timeout }); + } + + async checkElementVisibility(selector: string, timeout: number = 10000) { + await this.page.locator(selector).waitFor({ state: 'visible', timeout }); + } + + async checkElementBackgroundColor(selector: string, expectedColor: string) { + const element = this.page.locator(selector); + await element.waitFor({ state: 'visible' }); + const backgroundColor = await element.evaluate((el) => { + return window.getComputedStyle(el).backgroundColor; + }); + if (backgroundColor !== expectedColor) { + throw new Error(`Expected background color ${expectedColor}, but got ${backgroundColor}`); + } + } + + async clickElementWithText(selector: string, text: string) { + await this.page.locator(selector).filter({ hasText: text }).click(); + } + + async waitForDynamicImport(timeout: number = 5000) { + // Wait for any dynamic imports to complete + await this.page.waitForTimeout(1000); + await this.page.waitForLoadState('networkidle', { timeout }); + } + + async checkDateFormat() { + // Check for moment.js formatted date (MMMM Do YYYY, h:mm format) + const dateElement = this.page.locator('text=/[A-Z][a-z]+ \\d{1,2}[a-z]{2} \\d{4}, \\d{1,2}:\\d{2}/'); + await dateElement.waitFor({ timeout: 5000 }); + } + + getDateWithFormat(): string { + // Get current date in moment.js format: "MMMM Do YYYY, h:mm" + const now = new Date(); + const months = ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December']; + + const day = now.getDate(); + const suffix = day === 1 || day === 21 || day === 31 ? 'st' : + day === 2 || day === 22 ? 'nd' : + day === 3 || day === 23 ? 'rd' : 'th'; + + const month = months[now.getMonth()]; + const year = now.getFullYear(); + const hour = now.getHours() % 12 || 12; + const minute = now.getMinutes().toString().padStart(2, '0'); + + return `${month} ${day}${suffix} ${year}, ${hour}:${minute}`; + } +} + diff --git a/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/constants.ts b/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/constants.ts new file mode 100644 index 00000000000..3533a24a590 --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/constants.ts @@ -0,0 +1,25 @@ +export const Constants = { + elementsText: { + dynamicRemotesApp: { + header: '🌐 Dynamic System Host', + synchronousImportWidgetsNames: ['App 1 Widget', 'App 2 Widget'], + }, + }, + commonConstantsData: { + commonCountAppNames: { + app1: 'App 1 - Demonstrating Synchronous Imports with Dynamic Remotes', + app2: 'App 2', + }, + }, + commonPhrases: { + dynamicRemotesApp: { + widgetParagraphText: [ + "Moment shouldn't download twice", + "Moment shouldn't download twice" + ], + }, + }, + color: { + dynamicRemotesWidgetColor: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + }, +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/selectors.ts b/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/selectors.ts new file mode 100644 index 00000000000..8b62786ec6d --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/e2e/utils/selectors.ts @@ -0,0 +1,13 @@ +export const selectors = { + dataTestIds: { + app1Widget: '[data-e2e="WIDGET__1"]', + app2Widget: '[data-e2e="WIDGET__2"]', + }, + tags: { + headers: { + h1: 'h1', + h2: 'h2', + }, + paragraph: 'p', + }, +}; \ No newline at end of file diff --git a/advanced-api/dynamic-remotes-synchronous-imports/moduleConfig.js b/advanced-api/dynamic-remotes-synchronous-imports/moduleConfig.js index 1a0a6dbafcd..72d690ce634 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/moduleConfig.js +++ b/advanced-api/dynamic-remotes-synchronous-imports/moduleConfig.js @@ -1,28 +1,264 @@ +/** + * Enhanced Module Federation Configuration + * + * This configuration demonstrates advanced patterns for dynamic remote URL resolution + * while maintaining synchronous imports. Features include: + * - Dynamic URL resolution via window variables + * - Fallback URL support for resilience + * - Environment-based configuration + * - Multi-environment support (dev, staging, prod) + * - CDN and local development support + */ + const moduleFileName = 'remoteEntry.js'; -// Host module -const app1Module = { - fileName: moduleFileName, - name: 'app1', - port: 3001, +/** + * Environment detection and configuration + */ +const getEnvironment = () => { + if (typeof window !== 'undefined') { + return window.location.hostname === 'localhost' ? 'development' : 'production'; + } + return process.env.NODE_ENV || 'development'; }; -// Remote module -const app2Module = { - fileName: moduleFileName, - name: 'app2', - port: 3002, +/** + * Base configuration class for modules + */ +class ModuleConfig { + constructor(name, port, options = {}) { + this.name = name; + this.port = port; + this.fileName = options.fileName || moduleFileName; + this.environment = getEnvironment(); + this.cdnUrl = options.cdnUrl; + this.stagingUrl = options.stagingUrl; + } + + /** + * Get the base URL for this module based on environment + */ get url() { + switch (this.environment) { + case 'production': + return this.cdnUrl || `//cdn.example.com/${this.name}`; + case 'staging': + return this.stagingUrl || `//staging.example.com/${this.name}`; + default: + return `//localhost:${this.port}`; + } + } + + /** + * Get development URL (always localhost) + */ + get devUrl() { return `//localhost:${this.port}`; - }, + } + + /** + * Get the full remote entry URL + */ + get remoteEntryUrl() { + return `${this.url}/${this.fileName}`; + } +} + +// Host module configuration +const app1Module = new ModuleConfig('app1', 3001, { + cdnUrl: '//cdn.example.com/app1', + stagingUrl: '//staging.example.com/app1' +}); + +// Remote module configuration with enhanced features +const app2Module = new ModuleConfig('app2', 3002, { + cdnUrl: '//cdn.example.com/app2', + stagingUrl: '//staging.example.com/app2' +}); + +// Add dynamic configuration methods +Object.assign(app2Module, { + /** + * Global variable name that the runtime plugin will check + * The plugin looks for window[urlGlobalVariable] to override the URL + */ urlGlobalVariable: 'app2Url', + + /** + * Fallback global variable name for resilience + */ + fallbackGlobalVariable: 'app2FallbackUrl', + + /** + * Federation configuration string with placeholder syntax + * Format: remoteName@[window.globalVariableName]/remoteEntry.js + * + * The enhanced runtime plugin will: + * 1. Look for window.app2Url (primary) + * 2. If not found, look for window.app2FallbackUrl (fallback) + * 3. If neither found, use the default URL + * 4. Implement retry logic for failed loads + * + * This pattern enables resilient dynamic URL resolution while keeping synchronous imports + */ get federationConfig() { - // app2@[window.app2Url]/remoteEntry.js return `${this.name}@[window.${this.urlGlobalVariable}]/${this.fileName}`; }, + + /** + * Configuration with fallback support + * Uses the fallback URL if the primary fails + */ + get federationConfigWithFallback() { + return `${this.name}@[window.${this.fallbackGlobalVariable}]/${this.fileName}`; + }, + + /** + * Static configuration for development (no dynamic resolution) + * Useful for local development when you want predictable URLs + */ + get federationConfigStatic() { + return `${this.name}@${this.devUrl}/${this.fileName}`; + }, + + /** + * Get all possible URLs for this remote (for debugging/monitoring) + */ + getAllPossibleUrls() { + const urls = { + development: this.devUrl, + static: `${this.devUrl}/${this.fileName}`, + federationConfig: this.federationConfig + }; + + if (this.stagingUrl) { + urls.staging = `${this.stagingUrl}/${this.fileName}`; + } + + if (this.cdnUrl) { + urls.production = `${this.cdnUrl}/${this.fileName}`; + } + + // Add runtime URLs if available + if (typeof window !== 'undefined') { + if (window[this.urlGlobalVariable]) { + urls.runtimePrimary = `${window[this.urlGlobalVariable]}/${this.fileName}`; + } + if (window[this.fallbackGlobalVariable]) { + urls.runtimeFallback = `${window[this.fallbackGlobalVariable]}/${this.fileName}`; + } + } + + return urls; + } +}); + +/** + * Utility functions for dynamic remote configuration + */ +const configUtils = { + /** + * Set up dynamic URLs for all remotes + * Call this before your application starts to configure remote URLs + * + * @param {Object} remoteUrls - Object mapping remote names to URLs + * @example + * configUtils.setupDynamicUrls({ + * app2: '//production-server.com/app2', + * app3: '//another-server.com/app3' + * }); + */ + setupDynamicUrls(remoteUrls) { + if (typeof window === 'undefined') { + console.warn('[Module Config] Cannot setup dynamic URLs in non-browser environment'); + return; + } + + Object.entries(remoteUrls).forEach(([remoteName, url]) => { + const globalVarName = `${remoteName}Url`; + window[globalVarName] = url; + console.log(`[Module Config] Set ${globalVarName} = ${url}`); + }); + }, + + /** + * Set up fallback URLs for resilience + * + * @param {Object} fallbackUrls - Object mapping remote names to fallback URLs + */ + setupFallbackUrls(fallbackUrls) { + if (typeof window === 'undefined') { + console.warn('[Module Config] Cannot setup fallback URLs in non-browser environment'); + return; + } + + Object.entries(fallbackUrls).forEach(([remoteName, url]) => { + const globalVarName = `${remoteName}FallbackUrl`; + window[globalVarName] = url; + console.log(`[Module Config] Set fallback ${globalVarName} = ${url}`); + }); + }, + + /** + * Get current configuration for debugging + */ + getCurrentConfig() { + return { + environment: getEnvironment(), + app1: { + ...app1Module, + currentUrl: app1Module.url + }, + app2: { + ...app2Module, + currentUrl: app2Module.url, + allUrls: app2Module.getAllPossibleUrls() + } + }; + }, + + /** + * Validate remote URLs for common issues + */ + validateConfiguration() { + const issues = []; + + // Check if required ports are configured + if (app1Module.port === app2Module.port) { + issues.push('app1 and app2 are using the same port'); + } + + // Check if global variables are set when needed + if (typeof window !== 'undefined') { + const primaryUrl = window[app2Module.urlGlobalVariable]; + const fallbackUrl = window[app2Module.fallbackGlobalVariable]; + + if (!primaryUrl && !fallbackUrl) { + issues.push(`Neither ${app2Module.urlGlobalVariable} nor ${app2Module.fallbackGlobalVariable} is set`); + } + } + + return { + isValid: issues.length === 0, + issues + }; + } }; +// Export configuration and utilities module.exports = { app1Module, app2Module, + configUtils, + getEnvironment, + ModuleConfig }; + +// Make utilities available globally for debugging +if (typeof window !== 'undefined') { + window.__MF_CONFIG__ = { + ...module.exports, + getCurrentConfig: configUtils.getCurrentConfig, + validateConfiguration: configUtils.validateConfiguration + }; +} diff --git a/advanced-api/dynamic-remotes-synchronous-imports/package.json b/advanced-api/dynamic-remotes-synchronous-imports/package.json index 1058175297a..dd2e28ef70b 100644 --- a/advanced-api/dynamic-remotes-synchronous-imports/package.json +++ b/advanced-api/dynamic-remotes-synchronous-imports/package.json @@ -11,9 +11,11 @@ "build": "pnpm --filter dynamic-remotes-synchronous-imports_app* --parallel build", "serve": "pnpm --filter dynamic-remotes-synchronous-imports_app* --parallel serve", "clean": "pnpm --filter dynamic-remotes-synchronous-imports_app* --parallel clean", - "e2e:ci": "pnpm start & wait-on http-get://localhost:3001/ && npx cypress run --config-file ../../cypress-e2e/config/cypress.config.ts --config '{\"supportFile\": \"../../cypress-e2e/support/e2e.ts\"}' --spec \"./e2e/*.cy.ts\" --browser=chrome" + "e2e:ci": "npx playwright test", + "legacy:e2e:ci": "npx playwright test" }, "devDependencies": { + "@playwright/test": "^1.54.2", "wait-on": "7.2.0" } } diff --git a/advanced-api/dynamic-remotes-synchronous-imports/playwright.config.ts b/advanced-api/dynamic-remotes-synchronous-imports/playwright.config.ts new file mode 100644 index 00000000000..b506b75a73b --- /dev/null +++ b/advanced-api/dynamic-remotes-synchronous-imports/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + timeout: 60000, + expect: { + timeout: 15000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['list'], + ], + use: { + baseURL: 'http://localhost:3001', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + viewport: { width: 1920, height: 1080 }, + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: [ + { + command: 'pnpm --filter dynamic-remotes-synchronous-imports_app1 start', + port: 3001, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + { + command: 'pnpm --filter dynamic-remotes-synchronous-imports_app2 start', + port: 3002, + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, + ], +}); \ No newline at end of file diff --git a/advanced-api/dynamic-remotes/app1/package.json b/advanced-api/dynamic-remotes/app1/package.json index bb660262f4c..4114d595521 100644 --- a/advanced-api/dynamic-remotes/app1/package.json +++ b/advanced-api/dynamic-remotes/app1/package.json @@ -4,14 +4,19 @@ "devDependencies": { "@babel/core": "7.24.7", "@babel/preset-react": "7.24.7", + "@babel/preset-typescript": "^7.24.7", "@module-federation/enhanced": "0.17.1", "@module-federation/runtime": "0.17.1", "@rspack/cli": "1.4.11", "@rspack/core": "1.4.11", "@rspack/dev-server": "1.1.3", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "babel-loader": "9.1.3", "html-webpack-plugin": "5.6.0", "serve": "14.2.3", + "ts-loader": "^9.5.1", + "typescript": "^5.5.4", "webpack": "5.101.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.0.4" diff --git a/advanced-api/dynamic-remotes/app1/rspack.config.js b/advanced-api/dynamic-remotes/app1/rspack.config.js index cb91134113b..0e9c0829761 100644 --- a/advanced-api/dynamic-remotes/app1/rspack.config.js +++ b/advanced-api/dynamic-remotes/app1/rspack.config.js @@ -2,49 +2,50 @@ const { HtmlRspackPlugin, } = require('@rspack/core'); const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack') - const path = require('path'); +const { createSharedConfig, createDevServerConfig, swcConfig } = require('../shared-config'); module.exports = { entry: './src/index', mode: 'development', target: 'web', - devServer: { - static: { - directory: path.join(__dirname, 'dist'), - }, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', - 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization', - }, - port: 3001, - }, + devServer: createDevServerConfig(3001), output: { publicPath: 'auto', }, + resolve: { + extensions: ['.tsx', '.ts', '.jsx', '.js'], + alias: { + '@module-federation/runtime$': require.resolve('@module-federation/runtime'), + }, + }, module: { rules: [ { - test: /\.(js|jsx)$/, + test: /\.(ts|tsx)$/, include: path.resolve(__dirname, 'src'), use: { loader: 'builtin:swc-loader', options: { + ...swcConfig, jsc: { + ...swcConfig.jsc, parser: { - syntax: 'ecmascript', - jsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, + syntax: 'typescript', + tsx: true, }, }, }, }, }, + { + test: /\.(js|jsx)$/, + include: path.resolve(__dirname, 'src'), + use: { + loader: 'builtin:swc-loader', + options: swcConfig, + }, + }, ], }, plugins: [ @@ -54,36 +55,18 @@ module.exports = { // version is inferred from package.json // there is no version check for the required version // so it will always use the higher version found - shared: { - react: { - import: 'react', - shareKey: 'react', - shareScope: 'default', - singleton: true, - requiredVersion: '^18.3.1', - strictVersion: true, - }, - 'react/jsx-runtime': { - singleton: true, - }, - 'react/jsx-dev-runtime': { - singleton: true, - }, - 'react-dom': { - singleton: true, - requiredVersion: '^18.3.1', - strictVersion: true, - }, + shared: createSharedConfig(), + dts: { + generateTypes: true, + generateAPITypes: true, + }, + manifest: { + fileName: 'mf-manifest.json', + getPublicPath: () => 'auto', }, }), new HtmlRspackPlugin({ template: './public/index.html', }), ], - // it will be fixed soon... - resolve: { - alias: { - '@module-federation/runtime$': require.resolve('@module-federation/runtime'), - }, - }, }; diff --git a/advanced-api/dynamic-remotes/app1/src/App.js b/advanced-api/dynamic-remotes/app1/src/App.tsx similarity index 55% rename from advanced-api/dynamic-remotes/app1/src/App.js rename to advanced-api/dynamic-remotes/app1/src/App.tsx index fdb7fc71ce2..82dbbd6e1e1 100644 --- a/advanced-api/dynamic-remotes/app1/src/App.js +++ b/advanced-api/dynamic-remotes/app1/src/App.tsx @@ -1,21 +1,32 @@ import React, { useState, useEffect, Suspense } from 'react'; import { init, loadRemote } from '@module-federation/runtime'; +import { createDefaultPlugins } from '../../runtime-plugins'; +import type { RemoteComponentProps, DynamicImportHook } from '../../types/module-federation'; -class ErrorBoundary extends React.Component { - constructor(props) { +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; +} + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } - componentDidCatch(error, errorInfo) { + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { console.error('Remote component error:', error, errorInfo); } - render() { + render(): React.ReactNode { if (this.state.hasError) { return (
{ +const getRemoteEntry = (port: number): string => { const baseUrl = process.env.NODE_ENV === 'production' ? (process.env.REACT_APP_REMOTE_BASE_URL || window.location.origin) : 'http://localhost'; return `${baseUrl}:${port}/remoteEntry.js`; }; +// Initialize runtime with plugins for enhanced error handling and performance init({ name: 'app1', remotes: [ @@ -74,64 +86,96 @@ init({ entry: getRemoteEntry(3003), }, ], + plugins: createDefaultPlugins({ + retry: { + onRetry: (attempt, error, args) => { + console.log(`Retrying ${args.id} (attempt ${attempt}):`, error.message); + }, + onFailure: (error, args) => { + console.error(`Failed to load ${args.id} after all retries:`, error); + } + }, + performance: { + onSlowLoad: (loadTime, args) => { + console.warn(`Slow load detected for ${args.id}: ${loadTime}ms`); + } + }, + errorBoundary: { + onError: (errorInfo) => { + // In a real app, you might send this to an error reporting service + console.error('Module Federation Error Report:', errorInfo); + } + } + }) }); -function useDynamicImport({ module, scope }) { - const [component, setComponent] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); +function useDynamicImport({ module, scope }: RemoteComponentProps): DynamicImportHook { + const [component, setComponent] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + + const loadComponent = async (isRetry: boolean = false): Promise => { + if (isRetry) { + setRetryCount(prev => prev + 1); + } else { + setRetryCount(0); + } + + setLoading(true); + setError(null); + if (!isRetry) setComponent(null); + + try { + console.log(`Loading remote module: ${scope}/${module}${isRetry ? ` (retry ${retryCount + 1})` : ''}`); + const { default: Component } = await loadRemote(`${scope}/${module}`); + setComponent(() => Component); + console.log(`Successfully loaded: ${scope}/${module}`); + } catch (error) { + console.error(`Error loading remote module ${scope}/${module}:`, error); + setError(error as Error); + } finally { + setLoading(false); + } + }; useEffect(() => { if (!module || !scope) { setComponent(null); setError(null); + setRetryCount(0); return; } - const loadComponent = async () => { - setLoading(true); - setError(null); - setComponent(null); - - try { - console.log(`Loading remote module: ${scope}/${module}`); - const { default: Component } = await loadRemote(`${scope}/${module}`); - setComponent(() => Component); - console.log(`Successfully loaded: ${scope}/${module}`); - } catch (error) { - console.error(`Error loading remote module ${scope}/${module}:`, error); - setError(error); - } finally { - setLoading(false); - } - }; - loadComponent(); }, [module, scope]); - return { component, loading, error }; + return { component, loading, error, retryCount, retry: () => loadComponent(true) }; } -function App() { - const [{ module, scope }, setSystem] = useState({}); +function App(): JSX.Element { + const [{ module, scope }, setSystem] = useState>({}); - const setApp2 = () => { + const setApp2 = (): void => { setSystem({ scope: 'app2', module: 'Widget', }); }; - const setApp3 = () => { + const setApp3 = (): void => { setSystem({ scope: 'app3', module: 'Widget', }); }; - const { component: Component, loading, error } = useDynamicImport({ module, scope }); + const { component: Component, loading, error, retryCount, retry } = useDynamicImport({ + module: module || '', + scope: scope || '' + }); - const renderRemoteComponent = () => { + const renderRemoteComponent = (): React.ReactNode => { if (loading) { return (
🔄 Loading {scope}/{module}...
+ {retryCount > 0 && ( +
+ Retry attempt {retryCount} +
+ )}
); } @@ -157,6 +206,28 @@ function App() { }}>

⚠️ Failed to Load Remote Component

Could not load {scope}/{module}

+ {retryCount > 0 && ( +

+ Retry attempts: {retryCount} +

+ )} +
+ +
Error Details
diff --git a/advanced-api/dynamic-remotes/app1/tsconfig.json b/advanced-api/dynamic-remotes/app1/tsconfig.json
new file mode 100644
index 00000000000..8f061d58aa4
--- /dev/null
+++ b/advanced-api/dynamic-remotes/app1/tsconfig.json
@@ -0,0 +1,31 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "es6"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx"
+  },
+  "include": [
+    "src/**/*",
+    "../types/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/app1/webpack.config.js b/advanced-api/dynamic-remotes/app1/webpack.config.js
index 1e9c4b2810f..3107be662cb 100644
--- a/advanced-api/dynamic-remotes/app1/webpack.config.js
+++ b/advanced-api/dynamic-remotes/app1/webpack.config.js
@@ -1,34 +1,31 @@
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin;
 const path = require('path');
+const { createSharedConfig, createDevServerConfig, babelConfig } = require('../shared-config');
 
 module.exports = {
   entry: './src/index',
   mode: 'development',
   target: 'web',
-  devServer: {
-    static: {
-      directory: path.join(__dirname, 'dist'),
-    },
-    headers: {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
-      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
-    },
-    port: 3001,
-  },
+  devServer: createDevServerConfig(3001),
   output: {
     publicPath: 'auto',
   },
+  resolve: {
+    extensions: ['.tsx', '.ts', '.jsx', '.js'],
+  },
   module: {
     rules: [
+      {
+        test: /\.(ts|tsx)$/,
+        loader: 'ts-loader',
+        exclude: /node_modules/,
+      },
       {
         test: /\.jsx?$/,
         loader: 'babel-loader',
         exclude: /node_modules/,
-        options: {
-          presets: ['@babel/preset-react'],
-        },
+        options: babelConfig,
       },
     ],
   },
@@ -39,23 +36,14 @@ module.exports = {
       // version is inferred from package.json
       // there is no version check for the required version
       // so it will always use the higher version found
-      shared: {
-        react: {
-          import: 'react',
-          shareKey: 'react',
-          shareScope: 'default',
-          singleton: true,
-          requiredVersion: '^18.3.1',
-          strictVersion: true,
-        },
-        'react-dom': {
-          singleton: true,
-          requiredVersion: '^18.3.1',
-          strictVersion: true,
-        },
-        'react/jsx-runtime': {
-          singleton: true,
-        },
+      shared: createSharedConfig(),
+      dts: {
+        generateTypes: true,
+        generateAPITypes: true,
+      },
+      manifest: {
+        fileName: 'mf-manifest.json',
+        getPublicPath: () => 'auto',
       },
     }),
     new HtmlWebpackPlugin({
diff --git a/advanced-api/dynamic-remotes/app2/package.json b/advanced-api/dynamic-remotes/app2/package.json
index 5a8fe394821..d8b55adfa6b 100644
--- a/advanced-api/dynamic-remotes/app2/package.json
+++ b/advanced-api/dynamic-remotes/app2/package.json
@@ -4,13 +4,18 @@
   "devDependencies": {
     "@babel/core": "7.24.7",
     "@babel/preset-react": "7.24.7",
+    "@babel/preset-typescript": "^7.24.7",
     "@module-federation/enhanced": "0.17.1",
     "@rspack/cli": "1.4.11",
     "@rspack/core": "1.4.11",
     "@rspack/dev-server": "1.1.3",
+    "@types/react": "^18.3.3",
+    "@types/react-dom": "^18.3.0",
     "babel-loader": "9.1.3",
     "html-webpack-plugin": "5.6.0",
     "serve": "14.2.3",
+    "ts-loader": "^9.5.1",
+    "typescript": "^5.5.4",
     "webpack": "5.101.0",
     "webpack-cli": "5.1.4",
     "webpack-dev-server": "5.0.4"
diff --git a/advanced-api/dynamic-remotes/app2/rspack.config.js b/advanced-api/dynamic-remotes/app2/rspack.config.js
index 8b08b700066..54a77b1fba7 100644
--- a/advanced-api/dynamic-remotes/app2/rspack.config.js
+++ b/advanced-api/dynamic-remotes/app2/rspack.config.js
@@ -5,21 +5,12 @@ const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack')
 
 const path = require('path');
 const deps = require('./package.json').dependencies;
+const { createSharedConfig, createDevServerConfig, swcConfig } = require('../shared-config');
 module.exports = {
   entry: './src/index',
   mode: 'development',
   target: 'web',
-  devServer: {
-    static: {
-      directory: path.join(__dirname, 'dist'),
-    },
-    headers: {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
-      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
-    },
-    port: 3002,
-  },
+  devServer: createDevServerConfig(3002),
   output: {
     publicPath: 'auto',
   },
@@ -33,19 +24,7 @@ module.exports = {
         include: path.resolve(__dirname, 'src'),
         use: {
           loader: 'builtin:swc-loader',
-          options: {
-            jsc: {
-              parser: {
-                syntax: 'ecmascript',
-                jsx: true,
-              },
-              transform: {
-                react: {
-                  runtime: 'automatic',
-                },
-              },
-            },
-          },
+          options: swcConfig,
         },
       },
     ],
@@ -57,30 +36,19 @@ module.exports = {
       exposes: {
         './Widget': './src/Widget',
       },
-      shared: {
+      shared: createSharedConfig({
         moment: {
           requiredVersion: deps.moment,
           singleton: false,
         },
-        'react/jsx-runtime': {
-          singleton: true,
-        },
-        'react/jsx-dev-runtime': {
-          singleton: true,
-        },
-        react: {
-          requiredVersion: '^18.3.1',
-          import: 'react',
-          shareKey: 'react',
-          shareScope: 'default',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react-dom': {
-          requiredVersion: '^18.3.1',
-          singleton: true,
-          strictVersion: true,
-        },
+      }),
+      dts: {
+        generateTypes: false,
+        generateAPITypes: false,
+      },
+      manifest: {
+        fileName: 'mf-manifest.json',
+        getPublicPath: () => 'auto',
       },
     }),
     new HtmlRspackPlugin({
diff --git a/advanced-api/dynamic-remotes/app2/tsconfig.json b/advanced-api/dynamic-remotes/app2/tsconfig.json
new file mode 100644
index 00000000000..8f061d58aa4
--- /dev/null
+++ b/advanced-api/dynamic-remotes/app2/tsconfig.json
@@ -0,0 +1,31 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "es6"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx"
+  },
+  "include": [
+    "src/**/*",
+    "../types/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/app2/webpack.config.js b/advanced-api/dynamic-remotes/app2/webpack.config.js
index 7351c6ebbbd..37eb82a8e1d 100644
--- a/advanced-api/dynamic-remotes/app2/webpack.config.js
+++ b/advanced-api/dynamic-remotes/app2/webpack.config.js
@@ -2,21 +2,12 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
 const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin;
 const path = require('path');
 const deps = require('./package.json').dependencies;
+const { createSharedConfig, createDevServerConfig, babelConfig } = require('../shared-config');
 module.exports = {
   entry: './src/index',
   mode: 'development',
   target: 'web',
-  devServer: {
-    static: {
-      directory: path.join(__dirname, 'dist'),
-    },
-    headers: {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
-      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
-    },
-    port: 3002,
-  },
+  devServer: createDevServerConfig(3002),
 
   output: {
     publicPath: 'auto',
@@ -27,9 +18,7 @@ module.exports = {
         test: /\.jsx?$/,
         loader: 'babel-loader',
         exclude: /node_modules/,
-        options: {
-          presets: ['@babel/preset-react'],
-        },
+        options: babelConfig,
       },
     ],
   },
@@ -40,27 +29,19 @@ module.exports = {
       exposes: {
         './Widget': './src/Widget',
       },
-      shared: {
+      shared: createSharedConfig({
         moment: {
           requiredVersion: deps.moment,
           singleton: false,
         },
-        react: {
-          requiredVersion: '^18.3.1',
-          import: 'react',
-          shareKey: 'react',
-          shareScope: 'default',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react-dom': {
-          requiredVersion: '^18.3.1',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react/jsx-runtime': {
-          singleton: true,
-        },
+      }),
+      dts: {
+        generateTypes: true,
+        generateAPITypes: true,
+      },
+      manifest: {
+        fileName: 'mf-manifest.json',
+        getPublicPath: () => 'auto',
       },
     }),
     new HtmlWebpackPlugin({
diff --git a/advanced-api/dynamic-remotes/app3/package.json b/advanced-api/dynamic-remotes/app3/package.json
index c71dc2776e1..de077853503 100644
--- a/advanced-api/dynamic-remotes/app3/package.json
+++ b/advanced-api/dynamic-remotes/app3/package.json
@@ -4,13 +4,18 @@
   "devDependencies": {
     "@babel/core": "7.24.7",
     "@babel/preset-react": "7.24.7",
+    "@babel/preset-typescript": "^7.24.7",
     "@module-federation/enhanced": "0.17.1",
     "@rspack/cli": "1.4.11",
     "@rspack/core": "1.4.11",
     "@rspack/dev-server": "1.1.3",
+    "@types/react": "^18.3.3",
+    "@types/react-dom": "^18.3.0",
     "babel-loader": "9.1.3",
     "html-webpack-plugin": "5.6.0",
     "serve": "14.2.3",
+    "ts-loader": "^9.5.1",
+    "typescript": "^5.5.4",
     "webpack": "5.101.0",
     "webpack-cli": "5.1.4",
     "webpack-dev-server": "5.0.4"
diff --git a/advanced-api/dynamic-remotes/app3/rspack.config.js b/advanced-api/dynamic-remotes/app3/rspack.config.js
index bb5f0970a09..a6ed0365c6e 100644
--- a/advanced-api/dynamic-remotes/app3/rspack.config.js
+++ b/advanced-api/dynamic-remotes/app3/rspack.config.js
@@ -5,20 +5,11 @@ const {ModuleFederationPlugin} = require('@module-federation/enhanced/rspack')
 
 const path = require('path');
 const deps = require('./package.json').dependencies;
+const { createSharedConfig, createDevServerConfig, swcConfig } = require('../shared-config');
 module.exports = {
   entry: './src/index',
   mode: 'development',
-  devServer: {
-    static: {
-      directory: path.join(__dirname, 'dist'),
-    },
-    headers: {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
-      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
-    },
-    port: 3003,
-  },
+  devServer: createDevServerConfig(3003),
   target: 'web',
   output: {
     publicPath: 'auto',
@@ -30,19 +21,7 @@ module.exports = {
         include: path.resolve(__dirname, 'src'),
         use: {
           loader: 'builtin:swc-loader',
-          options: {
-            jsc: {
-              parser: {
-                syntax: 'ecmascript',
-                jsx: true,
-              },
-              transform: {
-                react: {
-                  runtime: 'automatic',
-                },
-              },
-            },
-          },
+          options: swcConfig,
         },
       },
     ],
@@ -54,26 +33,7 @@ module.exports = {
       exposes: {
         './Widget': './src/Widget',
       },
-      shared: {
-        react: {
-          requiredVersion: '^18.3.1',
-          import: 'react',
-          shareKey: 'react',
-          shareScope: 'default',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react-dom': {
-          requiredVersion: '^18.3.1',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react/jsx-runtime': {
-          singleton: true,
-        },
-        'react/jsx-dev-runtime': {
-          singleton: true,
-        },
+      shared: createSharedConfig({
         moment: {
           requiredVersion: deps.moment,
           singleton: false,
@@ -86,6 +46,14 @@ module.exports = {
           requiredVersion: deps.redux,
           singleton: true,
         },
+      }),
+      dts: {
+        generateTypes: false,
+        generateAPITypes: false,
+      },
+      manifest: {
+        fileName: 'mf-manifest.json',
+        getPublicPath: () => 'auto',
       },
     }),
     new HtmlRspackPlugin({
diff --git a/advanced-api/dynamic-remotes/app3/tsconfig.json b/advanced-api/dynamic-remotes/app3/tsconfig.json
new file mode 100644
index 00000000000..8f061d58aa4
--- /dev/null
+++ b/advanced-api/dynamic-remotes/app3/tsconfig.json
@@ -0,0 +1,31 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "es6"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx"
+  },
+  "include": [
+    "src/**/*",
+    "../types/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/app3/webpack.config.js b/advanced-api/dynamic-remotes/app3/webpack.config.js
index 46c78cb785c..1a0bb6cd52d 100644
--- a/advanced-api/dynamic-remotes/app3/webpack.config.js
+++ b/advanced-api/dynamic-remotes/app3/webpack.config.js
@@ -2,20 +2,11 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
 const ModuleFederationPlugin = require('@module-federation/enhanced').ModuleFederationPlugin;
 const path = require('path');
 const deps = require('./package.json').dependencies;
+const { createSharedConfig, createDevServerConfig, babelConfig } = require('../shared-config');
 module.exports = {
   entry: './src/index',
   mode: 'development',
-  devServer: {
-    static: {
-      directory: path.join(__dirname, 'dist'),
-    },
-    headers: {
-      'Access-Control-Allow-Origin': '*',
-      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
-      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
-    },
-    port: 3003,
-  },
+  devServer: createDevServerConfig(3003),
   target: 'web',
   output: {
     publicPath: 'auto',
@@ -26,9 +17,7 @@ module.exports = {
         test: /\.jsx?$/,
         loader: 'babel-loader',
         exclude: /node_modules/,
-        options: {
-          presets: ['@babel/preset-react'],
-        },
+        options: babelConfig,
       },
     ],
   },
@@ -39,23 +28,7 @@ module.exports = {
       exposes: {
         './Widget': './src/Widget',
       },
-      shared: {
-        react: {
-          requiredVersion: deps.react,
-          import: 'react',
-          shareKey: 'react',
-          shareScope: 'default',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react-dom': {
-          requiredVersion: '^18.3.1',
-          singleton: true,
-          strictVersion: true,
-        },
-        'react/jsx-runtime': {
-          singleton: true,
-        },
+      shared: createSharedConfig({
         moment: {
           requiredVersion: deps.moment,
           singleton: false,
@@ -68,6 +41,14 @@ module.exports = {
           requiredVersion: deps.redux,
           singleton: true,
         },
+      }),
+      dts: {
+        generateTypes: true,
+        generateAPITypes: true,
+      },
+      manifest: {
+        fileName: 'mf-manifest.json',
+        getPublicPath: () => 'auto',
       },
     }),
     new HtmlWebpackPlugin({
diff --git a/advanced-api/dynamic-remotes/e2e/checkDynamicRemotesApps.spec.ts b/advanced-api/dynamic-remotes/e2e/checkDynamicRemotesApps.spec.ts
index 1289765ab6f..d8daebb4902 100644
--- a/advanced-api/dynamic-remotes/e2e/checkDynamicRemotesApps.spec.ts
+++ b/advanced-api/dynamic-remotes/e2e/checkDynamicRemotesApps.spec.ts
@@ -1,28 +1,102 @@
-import { test, expect } from './utils/base-test';
-import { selectors } from './utils/selectors';
-import { Constants } from './utils/constants';
+import { test, expect, Page } from '@playwright/test';
+
+// Helper functions
+async function openLocalhost(page: Page, port: number) {
+  await page.goto(`http://localhost:${port}`);
+  await page.waitForLoadState('networkidle');
+  
+  // Wait for module federation to load (give it extra time for federated components)
+  await page.waitForTimeout(2000);
+  
+  // Wait for React to render
+  await page.waitForFunction(() => {
+    const elements = document.querySelectorAll('h1, h2, button, p');
+    return elements.length > 0;
+  }, { timeout: 30000 });
+}
+
+async function checkElementWithTextPresence(page: Page, selector: string, text: string) {
+  const element = page.locator(`${selector}:has-text("${text}")`);
+  await expect(element).toBeVisible();
+}
+
+async function clickElementWithText(page: Page, selector: string, text: string) {
+  const element = page.locator(`${selector}:has-text("${text}")`);
+  
+  // Wait for element to be ready
+  await element.waitFor({ state: 'visible', timeout: 10000 });
+  
+  // Remove any overlays that might interfere
+  await page.evaluate(() => {
+    const overlays = document.querySelectorAll('#webpack-dev-server-client-overlay, iframe[src*="webpack-dev-server"]');
+    overlays.forEach(overlay => overlay.remove());
+  });
+  
+  // Try clicking with retries
+  let attempts = 0;
+  while (attempts < 3) {
+    try {
+      await element.click({ timeout: 5000 });
+      break;
+    } catch (error) {
+      attempts++;
+      if (attempts >= 3) throw error;
+      await page.waitForTimeout(1000);
+    }
+  }
+  
+  // Wait for any dynamic loading to complete
+  await page.waitForTimeout(3000);
+}
+
+async function checkElementVisibility(page: Page, selector: string) {
+  const element = page.locator(selector);
+  await expect(element).toBeVisible();
+}
+
+async function checkElementBackgroundColor(page: Page, selector: string, expectedColor: string) {
+  const element = page.locator(selector);
+  await expect(element).toHaveCSS('background-color', expectedColor);
+}
+
+async function waitForDynamicImport(page: Page) {
+  // Wait for dynamic import to complete - looking for loading states to disappear
+  await page.waitForTimeout(3000); // Give time for dynamic loading
+  
+  // Wait for any network activity to settle
+  await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
+    // Ignore timeout - loading might already be complete
+  });
+}
+
+async function checkDateFormat(page: Page) {
+  // Check for moment.js formatted date (format: "Month Day Year, time")
+  const dateRegex = /\w+ \d+\w+ \d{4}, \d+:\d+/;
+  const textContent = await page.textContent('body');
+  expect(textContent).toMatch(dateRegex);
+}
 
 test.describe('Dynamic Remotes E2E Tests', () => {
   
   test.describe('Host Application (App 1)', () => {
-    test('should display host application elements correctly', async ({ basePage }) => {
+    test('should display host application elements correctly', async ({ page }) => {
       const consoleErrors: string[] = [];
-      basePage.page.on('console', (msg) => {
+      page.on('console', (msg) => {
         if (msg.type() === 'error') {
           consoleErrors.push(msg.text());
         }
       });
 
-      await basePage.openLocalhost(3001);
+      await openLocalhost(page, 3001);
 
       // Check main elements exist
-      await basePage.checkElementWithTextPresence('h1', 'Dynamic System Host');
-      await basePage.checkElementWithTextPresence('h2', 'App 1');
-      await basePage.checkElementWithTextPresence('p', 'The Dynamic System will take advantage of Module Federation');
+      await checkElementWithTextPresence(page, 'h1', 'Dynamic System Host');
+      await checkElementWithTextPresence(page, 'h2', 'App 1');
+      await checkElementWithTextPresence(page, 'p', 'The Dynamic System will take advantage of Module Federation');
 
       // Check both buttons exist
-      await basePage.checkElementWithTextPresence('button', 'Load App 2 Widget');
-      await basePage.checkElementWithTextPresence('button', 'Load App 3 Widget');
+      await checkElementWithTextPresence(page, 'button', 'Load App 2 Widget');
+      await checkElementWithTextPresence(page, 'button', 'Load App 3 Widget');
 
       // Verify no critical console errors
       const criticalErrors = consoleErrors.filter(error => 
@@ -33,27 +107,27 @@ test.describe('Dynamic Remotes E2E Tests', () => {
       expect(criticalErrors).toHaveLength(0);
     });
 
-    test('should dynamically load App 2 widget successfully', async ({ basePage }) => {
+    test('should dynamically load App 2 widget successfully', async ({ page }) => {
       const consoleErrors: string[] = [];
-      basePage.page.on('console', (msg) => {
+      page.on('console', (msg) => {
         if (msg.type() === 'error') {
           consoleErrors.push(msg.text());
         }
       });
 
-      await basePage.openLocalhost(3001);
+      await openLocalhost(page, 3001);
 
       // Click to load App 2 widget
-      await basePage.clickElementWithText('button', 'Load App 2 Widget');
-      await basePage.waitForDynamicImport();
+      await clickElementWithText(page, 'button', 'Load App 2 Widget');
+      await waitForDynamicImport(page);
 
       // Verify App 2 widget loaded
-      await basePage.checkElementVisibility(selectors.dataTestIds.app2Widget);
-      await basePage.checkElementWithTextPresence('h2', 'App 2 Widget');
-      await basePage.checkElementBackgroundColor(selectors.dataTestIds.app2Widget, 'rgb(255, 0, 0)');
+      await checkElementVisibility(page, '[data-e2e="APP_2__WIDGET"]');
+      await checkElementWithTextPresence(page, 'h2', 'App 2 Widget');
+      await checkElementBackgroundColor(page, '[data-e2e="APP_2__WIDGET"]', 'rgb(255, 0, 0)');
 
       // Check for moment.js date formatting
-      await basePage.checkDateFormat();
+      await checkDateFormat(page);
 
       // Verify no module federation errors
       const moduleErrors = consoleErrors.filter(error => 
@@ -63,27 +137,27 @@ test.describe('Dynamic Remotes E2E Tests', () => {
       expect(moduleErrors).toHaveLength(0);
     });
 
-    test('should dynamically load App 3 widget successfully', async ({ basePage }) => {
+    test('should dynamically load App 3 widget successfully', async ({ page }) => {
       const consoleErrors: string[] = [];
-      basePage.page.on('console', (msg) => {
+      page.on('console', (msg) => {
         if (msg.type() === 'error') {
           consoleErrors.push(msg.text());
         }
       });
 
-      await basePage.openLocalhost(3001);
+      await openLocalhost(page, 3001);
 
       // Click to load App 3 widget
-      await basePage.clickElementWithText('button', 'Load App 3 Widget');
-      await basePage.waitForDynamicImport();
+      await clickElementWithText(page, 'button', 'Load App 3 Widget');
+      await waitForDynamicImport(page);
 
       // Verify App 3 widget loaded
-      await basePage.checkElementVisibility(selectors.dataTestIds.app3Widget);
-      await basePage.checkElementWithTextPresence('h2', 'App 3 Widget');
-      await basePage.checkElementBackgroundColor(selectors.dataTestIds.app3Widget, 'rgb(128, 0, 128)');
+      await checkElementVisibility(page, '[data-e2e="APP_3__WIDGET"]');
+      await checkElementWithTextPresence(page, 'h2', 'App 3 Widget');
+      await checkElementBackgroundColor(page, '[data-e2e="APP_3__WIDGET"]', 'rgb(128, 0, 128)');
 
       // Check for moment.js date formatting
-      await basePage.checkDateFormat();
+      await checkDateFormat(page);
 
       // Verify no module federation errors
       const moduleErrors = consoleErrors.filter(error => 
@@ -93,61 +167,61 @@ test.describe('Dynamic Remotes E2E Tests', () => {
       expect(moduleErrors).toHaveLength(0);
     });
 
-    test('should handle sequential loading of both widgets', async ({ basePage }) => {
-      await basePage.openLocalhost(3001);
+    test('should handle sequential loading of both widgets', async ({ page }) => {
+      await openLocalhost(page, 3001);
 
       // Load App 2 widget first
-      await basePage.clickElementWithText('button', 'Load App 2 Widget');
-      await basePage.waitForDynamicImport();
+      await clickElementWithText(page, 'button', 'Load App 2 Widget');
+      await waitForDynamicImport(page);
       
       // Verify App 2 widget is loaded and get its content
-      await basePage.checkElementVisibility(selectors.dataTestIds.app2Widget);
-      await basePage.checkElementWithTextPresence('h2', 'App 2 Widget');
+      await checkElementVisibility(page, '[data-e2e="APP_2__WIDGET"]');
+      await checkElementWithTextPresence(page, 'h2', 'App 2 Widget');
 
       // Then load App 3 widget (this replaces the previous widget in this implementation)
-      await basePage.clickElementWithText('button', 'Load App 3 Widget');
-      await basePage.waitForDynamicImport();
+      await clickElementWithText(page, 'button', 'Load App 3 Widget');
+      await waitForDynamicImport(page);
       
       // Verify App 3 widget is loaded
-      await basePage.checkElementVisibility(selectors.dataTestIds.app3Widget);
-      await basePage.checkElementWithTextPresence('h2', 'App 3 Widget');
+      await checkElementVisibility(page, '[data-e2e="APP_3__WIDGET"]');
+      await checkElementWithTextPresence(page, 'h2', 'App 3 Widget');
 
       // Note: In this dynamic remotes implementation, widgets replace each other
       // rather than accumulating, so we verify the latest widget is visible
     });
 
-    test('should show loading states and handle errors gracefully', async ({ basePage }) => {
-      await basePage.openLocalhost(3001);
+    test('should show loading states and handle errors gracefully', async ({ page }) => {
+      await openLocalhost(page, 3001);
 
       // Check that buttons are initially enabled
-      const app2Button = basePage.page.locator('button').filter({ hasText: 'Load App 2 Widget' });
+      const app2Button = page.locator('button').filter({ hasText: 'Load App 2 Widget' });
       await expect(app2Button).toBeEnabled();
 
       // Monitor for any error boundaries or error states
-      const errorMessages = basePage.page.locator('text="⚠️"');
+      const errorMessages = page.locator('text="⚠️"');
       await expect(errorMessages).toHaveCount(0);
     });
   });
 
   test.describe('Remote Application - App 2', () => {
-    test('should display App 2 standalone correctly', async ({ basePage }) => {
+    test('should display App 2 standalone correctly', async ({ page }) => {
       const consoleErrors: string[] = [];
-      basePage.page.on('console', (msg) => {
+      page.on('console', (msg) => {
         if (msg.type() === 'error') {
           consoleErrors.push(msg.text());
         }
       });
 
-      await basePage.openLocalhost(3002);
+      await openLocalhost(page, 3002);
 
       // Check App 2 widget displays correctly when accessed directly
-      await basePage.checkElementVisibility(selectors.dataTestIds.app2Widget);
-      await basePage.checkElementWithTextPresence('h2', 'App 2 Widget');
-      await basePage.checkElementBackgroundColor(selectors.dataTestIds.app2Widget, 'rgb(255, 0, 0)');
+      await checkElementVisibility(page, '[data-e2e="APP_2__WIDGET"]');
+      await checkElementWithTextPresence(page, 'h2', 'App 2 Widget');
+      await checkElementBackgroundColor(page, '[data-e2e="APP_2__WIDGET"]', 'rgb(255, 0, 0)');
 
       // Check moment.js functionality
-      await basePage.checkElementWithTextPresence('p', "Moment shouldn't download twice");
-      await basePage.checkDateFormat();
+      await checkElementWithTextPresence(page, 'p', "Moment shouldn't download twice");
+      await checkDateFormat(page);
 
       // Verify no critical console errors (filter out expected warnings)
       const criticalErrors = consoleErrors.filter(e => 
@@ -161,23 +235,23 @@ test.describe('Dynamic Remotes E2E Tests', () => {
   });
 
   test.describe('Remote Application - App 3', () => {
-    test('should display App 3 standalone correctly', async ({ basePage }) => {
+    test('should display App 3 standalone correctly', async ({ page }) => {
       const consoleErrors: string[] = [];
-      basePage.page.on('console', (msg) => {
+      page.on('console', (msg) => {
         if (msg.type() === 'error') {
           consoleErrors.push(msg.text());
         }
       });
 
-      await basePage.openLocalhost(3003);
+      await openLocalhost(page, 3003);
 
       // Check App 3 widget displays correctly when accessed directly
-      await basePage.checkElementVisibility(selectors.dataTestIds.app3Widget);
-      await basePage.checkElementWithTextPresence('h2', 'App 3 Widget');
-      await basePage.checkElementBackgroundColor(selectors.dataTestIds.app3Widget, 'rgb(128, 0, 128)');
+      await checkElementVisibility(page, '[data-e2e="APP_3__WIDGET"]');
+      await checkElementWithTextPresence(page, 'h2', 'App 3 Widget');
+      await checkElementBackgroundColor(page, '[data-e2e="APP_3__WIDGET"]', 'rgb(128, 0, 128)');
 
       // Check for moment.js date formatting
-      await basePage.checkDateFormat();
+      await checkDateFormat(page);
 
       // Verify no critical console errors (filter out expected warnings)
       const criticalErrors = consoleErrors.filter(e => 
diff --git a/advanced-api/dynamic-remotes/e2e/utils/base-test.ts b/advanced-api/dynamic-remotes/e2e/utils/base-test.ts
index c1282ae9330..9bfbce676cf 100644
--- a/advanced-api/dynamic-remotes/e2e/utils/base-test.ts
+++ b/advanced-api/dynamic-remotes/e2e/utils/base-test.ts
@@ -1,4 +1,4 @@
-import { test as base, expect, Page } from '@playwright/test';
+import { Page } from '@playwright/test';
 
 export class BasePage {
   constructor(public page: Page) {}
@@ -87,11 +87,5 @@ export class BasePage {
   }
 }
 
-export const test = base.extend<{ basePage: BasePage }>({
-  basePage: async ({ page }, use) => {
-    const basePage = new BasePage(page);
-    await use(basePage);
-  },
-});
 
 export { expect } from '@playwright/test';
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/package.json b/advanced-api/dynamic-remotes/package.json
index 59b9ca20adf..ee8a8e2ccbc 100644
--- a/advanced-api/dynamic-remotes/package.json
+++ b/advanced-api/dynamic-remotes/package.json
@@ -17,8 +17,8 @@
     "test:e2e": "npx playwright test",
     "test:e2e:ui": "npx playwright test --ui",
     "test:e2e:debug": "npx playwright test --debug",
-    "e2e:ci": "pnpm start & sleep 5 && wait-on tcp:3001 tcp:3002 tcp:3003 && npx playwright test --reporter=list; kill $(jobs -p) 2>/dev/null || true",
-    "legacy:e2e:ci": "pnpm legacy:start & sleep 5 && wait-on tcp:3001 tcp:3002 tcp:3003 && LEGACY_MODE=true npx playwright test --reporter=list; kill $(jobs -p) 2>/dev/null || true"
+    "e2e:ci": "npx playwright test --reporter=list",
+    "legacy:e2e:ci": "npx playwright test --reporter=list"
   },
   "devDependencies": {
     "@playwright/test": "^1.54.2",
diff --git a/advanced-api/dynamic-remotes/playwright.config.ts b/advanced-api/dynamic-remotes/playwright.config.ts
index 26445e39243..a8fd326e740 100644
--- a/advanced-api/dynamic-remotes/playwright.config.ts
+++ b/advanced-api/dynamic-remotes/playwright.config.ts
@@ -29,6 +29,24 @@ export default defineConfig({
     },
   ],
 
-  // webServer configuration removed - servers are started manually in package.json scripts
-  // This ensures better compatibility with CI environments and matches the original Cypress approach
+  webServer: [
+    {
+      command: 'pnpm --filter dynamic-remotes_app1 start',
+      port: 3001,
+      reuseExistingServer: !process.env.CI,
+      timeout: 120000,
+    },
+    {
+      command: 'pnpm --filter dynamic-remotes_app2 start',
+      port: 3002,
+      reuseExistingServer: !process.env.CI,
+      timeout: 120000,
+    },
+    {
+      command: 'pnpm --filter dynamic-remotes_app3 start',
+      port: 3003,
+      reuseExistingServer: !process.env.CI,
+      timeout: 120000,
+    },
+  ],
 });
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/runtime-plugins.js b/advanced-api/dynamic-remotes/runtime-plugins.js
new file mode 100644
index 00000000000..0d7a5618774
--- /dev/null
+++ b/advanced-api/dynamic-remotes/runtime-plugins.js
@@ -0,0 +1,279 @@
+/**
+ * Module Federation Runtime Plugins
+ * 
+ * These plugins provide enhanced error handling, retry mechanisms,
+ * and performance monitoring for dynamic remote loading.
+ */
+
+/**
+ * Retry Plugin for failed remote loads
+ * Implements exponential backoff retry strategy
+ */
+class RetryPlugin {
+  constructor(options = {}) {
+    this.maxRetries = options.maxRetries || 3;
+    this.baseDelay = options.baseDelay || 1000;
+    this.maxDelay = options.maxDelay || 5000;
+    this.onRetry = options.onRetry || (() => {});
+    this.onFailure = options.onFailure || (() => {});
+  }
+
+  name = 'retry-plugin';
+
+  async loadRemote(args, next) {
+    let lastError;
+    
+    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
+      try {
+        return await next(args);
+      } catch (error) {
+        lastError = error;
+        
+        if (attempt === this.maxRetries - 1) {
+          console.error(`Failed to load remote after ${this.maxRetries} attempts:`, error);
+          this.onFailure(error, args);
+          throw error;
+        }
+
+        const delay = Math.min(
+          this.baseDelay * Math.pow(2, attempt),
+          this.maxDelay
+        );
+        
+        console.warn(`Retry attempt ${attempt + 1}/${this.maxRetries} for remote ${args.id} in ${delay}ms`, error);
+        this.onRetry(attempt + 1, error, args);
+        
+        await new Promise(resolve => setTimeout(resolve, delay));
+      }
+    }
+    
+    throw lastError;
+  }
+}
+
+/**
+ * Performance Monitoring Plugin
+ * Tracks remote loading performance and network issues
+ */
+class PerformancePlugin {
+  constructor(options = {}) {
+    this.enableLogging = options.enableLogging !== false;
+    this.slowThreshold = options.slowThreshold || 3000;
+    this.onSlowLoad = options.onSlowLoad || (() => {});
+    this.onLoadSuccess = options.onLoadSuccess || (() => {});
+  }
+
+  name = 'performance-plugin';
+
+  async loadRemote(args, next) {
+    const startTime = performance.now();
+    
+    try {
+      const result = await next(args);
+      const loadTime = performance.now() - startTime;
+      
+      if (this.enableLogging) {
+        console.log(`Successfully loaded remote ${args.id} in ${loadTime.toFixed(2)}ms`);
+      }
+      
+      if (loadTime > this.slowThreshold) {
+        this.onSlowLoad(loadTime, args);
+      }
+      
+      this.onLoadSuccess(loadTime, args);
+      return result;
+    } catch (error) {
+      const loadTime = performance.now() - startTime;
+      console.error(`Failed to load remote ${args.id} after ${loadTime.toFixed(2)}ms:`, error);
+      throw error;
+    }
+  }
+}
+
+/**
+ * Health Check Plugin
+ * Validates remote availability before attempting to load
+ */
+class HealthCheckPlugin {
+  constructor(options = {}) {
+    this.timeout = options.timeout || 5000;
+    this.enableCheck = options.enableCheck !== false;
+    this.onHealthCheckFail = options.onHealthCheckFail || (() => {});
+  }
+
+  name = 'health-check-plugin';
+
+  async loadRemote(args, next) {
+    if (!this.enableCheck) {
+      return next(args);
+    }
+
+    try {
+      // Basic health check by trying to fetch the remote entry
+      const controller = new AbortController();
+      const timeoutId = setTimeout(() => controller.abort(), this.timeout);
+      
+      const response = await fetch(args.url, {
+        method: 'HEAD',
+        signal: controller.signal,
+        cache: 'no-cache',
+      });
+      
+      clearTimeout(timeoutId);
+      
+      if (!response.ok) {
+        throw new Error(`Health check failed: ${response.status} ${response.statusText}`);
+      }
+      
+      return await next(args);
+    } catch (error) {
+      console.warn(`Health check failed for remote ${args.id}:`, error.message);
+      this.onHealthCheckFail(error, args);
+      
+      // Still attempt to load, as the health check might fail due to CORS
+      // but the actual module loading might work
+      return await next(args);
+    }
+  }
+}
+
+/**
+ * Error Boundary Plugin
+ * Provides comprehensive error tracking and reporting
+ */
+class ErrorBoundaryPlugin {
+  constructor(options = {}) {
+    this.onError = options.onError || (() => {});
+    this.errorReporting = options.errorReporting !== false;
+    this.maxErrorReports = options.maxErrorReports || 10;
+    this.errorCounts = new Map();
+  }
+
+  name = 'error-boundary-plugin';
+
+  async loadRemote(args, next) {
+    try {
+      return await next(args);
+    } catch (error) {
+      const errorKey = `${args.id}-${error.name}`;
+      const errorCount = (this.errorCounts.get(errorKey) || 0) + 1;
+      this.errorCounts.set(errorKey, errorCount);
+      
+      const errorInfo = {
+        remoteId: args.id,
+        url: args.url,
+        error: error.message,
+        stack: error.stack,
+        timestamp: new Date().toISOString(),
+        count: errorCount,
+        userAgent: navigator.userAgent,
+      };
+      
+      if (this.errorReporting && errorCount <= this.maxErrorReports) {
+        console.error('Module Federation Error:', errorInfo);
+        this.onError(errorInfo);
+      }
+      
+      throw error;
+    }
+  }
+}
+
+/**
+ * Cache Plugin
+ * Implements intelligent caching for remote modules
+ */
+class CachePlugin {
+  constructor(options = {}) {
+    this.cacheTTL = options.cacheTTL || 5 * 60 * 1000; // 5 minutes
+    this.maxCacheSize = options.maxCacheSize || 50;
+    this.cache = new Map();
+    this.cacheTimestamps = new Map();
+  }
+
+  name = 'cache-plugin';
+
+  async loadRemote(args, next) {
+    const cacheKey = `${args.id}-${args.url}`;
+    const now = Date.now();
+    
+    // Check if we have a valid cached version
+    if (this.cache.has(cacheKey)) {
+      const cacheTime = this.cacheTimestamps.get(cacheKey);
+      if (now - cacheTime < this.cacheTTL) {
+        console.log(`Loading remote ${args.id} from cache`);
+        return this.cache.get(cacheKey);
+      } else {
+        // Cache expired
+        this.cache.delete(cacheKey);
+        this.cacheTimestamps.delete(cacheKey);
+      }
+    }
+    
+    // Load fresh and cache
+    const result = await next(args);
+    
+    // Implement LRU cache size limit
+    if (this.cache.size >= this.maxCacheSize) {
+      const oldestKey = this.cache.keys().next().value;
+      this.cache.delete(oldestKey);
+      this.cacheTimestamps.delete(oldestKey);
+    }
+    
+    this.cache.set(cacheKey, result);
+    this.cacheTimestamps.set(cacheKey, now);
+    
+    return result;
+  }
+}
+
+/**
+ * Create default runtime plugins with sensible defaults
+ */
+function createDefaultPlugins(customOptions = {}) {
+  const options = {
+    retry: {
+      maxRetries: 3,
+      baseDelay: 1000,
+      maxDelay: 5000,
+      ...customOptions.retry,
+    },
+    performance: {
+      enableLogging: process.env.NODE_ENV === 'development',
+      slowThreshold: 3000,
+      ...customOptions.performance,
+    },
+    healthCheck: {
+      timeout: 5000,
+      enableCheck: process.env.NODE_ENV === 'development',
+      ...customOptions.healthCheck,
+    },
+    errorBoundary: {
+      errorReporting: true,
+      maxErrorReports: 10,
+      ...customOptions.errorBoundary,
+    },
+    cache: {
+      cacheTTL: 5 * 60 * 1000,
+      maxCacheSize: 50,
+      ...customOptions.cache,
+    },
+  };
+
+  return [
+    new CachePlugin(options.cache),
+    new HealthCheckPlugin(options.healthCheck),
+    new PerformancePlugin(options.performance),
+    new RetryPlugin(options.retry),
+    new ErrorBoundaryPlugin(options.errorBoundary),
+  ];
+}
+
+module.exports = {
+  RetryPlugin,
+  PerformancePlugin,
+  HealthCheckPlugin,
+  ErrorBoundaryPlugin,
+  CachePlugin,
+  createDefaultPlugins,
+};
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/shared-config.js b/advanced-api/dynamic-remotes/shared-config.js
new file mode 100644
index 00000000000..2a05c0ffd08
--- /dev/null
+++ b/advanced-api/dynamic-remotes/shared-config.js
@@ -0,0 +1,158 @@
+/**
+ * Shared Module Federation Configuration
+ * 
+ * This file provides standardized shared dependency configurations
+ * across all applications in the dynamic remotes example.
+ */
+
+const deps = {
+  react: '^18.3.1',
+  'react-dom': '^18.3.1',
+  moment: '^2.29.4',
+  'react-redux': '^9.1.2',
+  redux: '^5.0.1',
+};
+
+/**
+ * Standard shared configuration for Module Federation
+ * @param {Object} additionalShared - Additional shared dependencies specific to the app
+ * @returns {Object} Complete shared configuration
+ */
+function createSharedConfig(additionalShared = {}) {
+  const baseShared = {
+    react: {
+      singleton: true,
+      requiredVersion: deps.react,
+      strictVersion: true,
+      eager: false,
+      import: 'react',
+      shareKey: 'react',
+      shareScope: 'default',
+    },
+    'react-dom': {
+      singleton: true,
+      requiredVersion: deps['react-dom'],
+      strictVersion: true,
+      eager: false,
+    },
+    'react/jsx-runtime': {
+      singleton: true,
+      eager: false,
+    },
+    'react/jsx-dev-runtime': {
+      singleton: true,
+      eager: false,
+    },
+  };
+
+  return {
+    ...baseShared,
+    ...additionalShared,
+  };
+}
+
+/**
+ * Environment-aware remote URL generation
+ * @param {number} port - Port number for the remote
+ * @param {string} appName - Name of the application
+ * @returns {string} Complete remote entry URL
+ */
+function getRemoteEntry(port, appName = '') {
+  const isDevelopment = process.env.NODE_ENV !== 'production';
+  const baseUrl = isDevelopment 
+    ? (process.env.REMOTE_BASE_URL || 'http://localhost')
+    : (process.env.REMOTE_BASE_URL || window?.location?.origin || 'http://localhost');
+  
+  return `${baseUrl}:${port}/remoteEntry.js`;
+}
+
+/**
+ * Standard webpack devServer configuration
+ * @param {number} port - Port number for the dev server
+ * @returns {Object} DevServer configuration
+ */
+function createDevServerConfig(port) {
+  return {
+    static: {
+      directory: require('path').join(process.cwd(), 'dist'),
+    },
+    headers: {
+      'Access-Control-Allow-Origin': '*',
+      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
+      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
+    },
+    port,
+    hot: true,
+    liveReload: false,
+    allowedHosts: 'all',
+    client: {
+      overlay: {
+        errors: true,
+        warnings: false,
+      },
+    },
+  };
+}
+
+/**
+ * Standard babel configuration for React with TypeScript
+ */
+const babelConfig = {
+  presets: [
+    ['@babel/preset-env', { targets: 'defaults' }],
+    ['@babel/preset-react', { runtime: 'automatic' }],
+    ['@babel/preset-typescript', { 
+      allowDeclareFields: true,
+      allowNamespaces: true 
+    }],
+  ],
+  plugins: [
+    ['@babel/plugin-proposal-class-properties'],
+  ],
+};
+
+/**
+ * Standard SWC configuration for Rspack with TypeScript support
+ */
+const swcConfig = {
+  jsc: {
+    parser: {
+      syntax: 'ecmascript',
+      jsx: true,
+    },
+    transform: {
+      react: {
+        runtime: 'automatic',
+      },
+    },
+    target: 'es2020',
+  },
+};
+
+/**
+ * TypeScript-specific SWC configuration
+ */
+const swcConfigTS = {
+  jsc: {
+    parser: {
+      syntax: 'typescript',
+      tsx: true,
+    },
+    transform: {
+      react: {
+        runtime: 'automatic',
+      },
+    },
+    target: 'es2020',
+  },
+};
+
+module.exports = {
+  createSharedConfig,
+  getRemoteEntry,
+  createDevServerConfig,
+  babelConfig,
+  swcConfig,
+  swcConfigTS,
+  deps,
+};
\ No newline at end of file
diff --git a/advanced-api/dynamic-remotes/types/module-federation.d.ts b/advanced-api/dynamic-remotes/types/module-federation.d.ts
new file mode 100644
index 00000000000..8baf3c344bf
--- /dev/null
+++ b/advanced-api/dynamic-remotes/types/module-federation.d.ts
@@ -0,0 +1,181 @@
+/**
+ * Module Federation Type Definitions
+ * 
+ * Provides type safety for remote modules and federation APIs
+ */
+
+declare module '@module-federation/runtime' {
+  export interface FederationRuntimePlugin {
+    name: string;
+    init?(args: InitOptions): void | Promise;
+    loadRemote?(args: LoadRemoteArgs, next: (args: LoadRemoteArgs) => Promise): Promise;
+  }
+
+  export interface InitOptions {
+    name: string;
+    remotes?: RemoteConfig[];
+    plugins?: FederationRuntimePlugin[];
+    shared?: SharedConfig;
+  }
+
+  export interface RemoteConfig {
+    name: string;
+    entry: string;
+    alias?: string;
+  }
+
+  export interface LoadRemoteArgs {
+    id: string;
+    url: string;
+    from?: string;
+    remoteName?: string;
+  }
+
+  export interface SharedConfig {
+    [key: string]: SharedDependency;
+  }
+
+  export interface SharedDependency {
+    singleton?: boolean;
+    requiredVersion?: string;
+    strictVersion?: boolean;
+    eager?: boolean;
+    import?: string;
+    shareKey?: string;
+    shareScope?: string;
+  }
+
+  export function init(options: InitOptions): Promise;
+  export function loadRemote(id: string): Promise<{ default: T }>;
+  export function registerRemotes(remotes: RemoteConfig[]): void;
+  export function getRemotes(): RemoteConfig[];
+}
+
+declare module '@module-federation/enhanced' {
+  export interface ModuleFederationPluginOptions {
+    name: string;
+    filename?: string;
+    exposes?: Record;
+    remotes?: Record;
+    shared?: SharedConfig;
+    library?: {
+      type: string;
+      name?: string;
+    };
+    dts?: {
+      generateTypes?: boolean;
+      generateAPITypes?: boolean;
+      consumeAPITypes?: boolean;
+      typesFolder?: string;
+    };
+    manifest?: {
+      fileName?: string;
+      getPublicPath?: () => string;
+    };
+    runtime?: boolean;
+    experiments?: {
+      federationRuntime?: string;
+    };
+  }
+
+  export class ModuleFederationPlugin {
+    constructor(options: ModuleFederationPluginOptions);
+  }
+
+  export interface SharedConfig {
+    [key: string]: SharedDependency;
+  }
+
+  export interface SharedDependency {
+    singleton?: boolean;
+    requiredVersion?: string;
+    strictVersion?: boolean;
+    eager?: boolean;
+    import?: string;
+    shareKey?: string;
+    shareScope?: string;
+  }
+}
+
+declare module '@module-federation/enhanced/rspack' {
+  export * from '@module-federation/enhanced';
+}
+
+// Global type declarations for remote modules
+declare global {
+  interface Window {
+    __FEDERATION__: {
+      remotes: Record;
+      shared: Record;
+    };
+  }
+}
+
+// Remote module declarations
+declare module 'app2/Widget' {
+  import { ComponentType } from 'react';
+  const Widget: ComponentType;
+  export default Widget;
+}
+
+declare module 'app3/Widget' {
+  import { ComponentType } from 'react';
+  const Widget: ComponentType;
+  export default Widget;
+}
+
+// Type utilities for dynamic remote loading
+export interface RemoteComponentProps {
+  module: string;
+  scope: string;
+}
+
+export interface DynamicImportHook {
+  component: React.ComponentType | null;
+  loading: boolean;
+  error: Error | null;
+  retryCount: number;
+  retry: () => void;
+}
+
+export interface RuntimePluginOptions {
+  retry?: {
+    maxRetries?: number;
+    baseDelay?: number;
+    maxDelay?: number;
+    onRetry?: (attempt: number, error: Error, args: LoadRemoteArgs) => void;
+    onFailure?: (error: Error, args: LoadRemoteArgs) => void;
+  };
+  performance?: {
+    enableLogging?: boolean;
+    slowThreshold?: number;
+    onSlowLoad?: (loadTime: number, args: LoadRemoteArgs) => void;
+    onLoadSuccess?: (loadTime: number, args: LoadRemoteArgs) => void;
+  };
+  healthCheck?: {
+    timeout?: number;
+    enableCheck?: boolean;
+    onHealthCheckFail?: (error: Error, args: LoadRemoteArgs) => void;
+  };
+  errorBoundary?: {
+    errorReporting?: boolean;
+    maxErrorReports?: number;
+    onError?: (errorInfo: ErrorInfo) => void;
+  };
+  cache?: {
+    cacheTTL?: number;
+    maxCacheSize?: number;
+  };
+}
+
+export interface ErrorInfo {
+  remoteId: string;
+  url: string;
+  error: string;
+  stack?: string;
+  timestamp: string;
+  count: number;
+  userAgent: string;
+}
+
+export {};
\ No newline at end of file
diff --git a/esm/rspack/webpack.config.mjs b/esm/rspack/webpack.config.mjs
deleted file mode 100644
index 37175549762..00000000000
--- a/esm/rspack/webpack.config.mjs
+++ /dev/null
@@ -1,120 +0,0 @@
-import { dirname } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import HtmlWebpackPlugin from 'html-webpack-plugin';
-import enhancedFederation from '@module-federation/enhanced';
-const { ModuleFederationPlugin } = enhancedFederation;
-import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-
-// Target browsers, see: https://github.com/browserslist/browserslist
-const targets = ['chrome >= 87', 'edge >= 88', 'firefox >= 78', 'safari >= 14'];
-
-export default {
-  context: __dirname,
-  entry: {
-    main: './src/main.js',
-    app: './src/other.jsx',
-    apeep: './src/App.jsx',
-  },
-  mode: 'development',
-  resolve: {
-    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
-  },
-  devServer: {
-    devMiddleware: {
-      writeToDisk: true
-    },
-    hot: true,
-    port: 8080,
-  },
-  module: {
-    rules: [
-      {
-        test: /\.svg$/,
-        type: 'asset',
-      },
-      {
-        test: /\.(jsx?|tsx?)$/,
-        exclude: /node_modules/,
-        use: [
-          {
-            loader: 'swc-loader',
-            options: {
-              jsc: {
-                parser: {
-                  syntax: 'typescript',
-                  tsx: true,
-                },
-                transform: {
-                  react: {
-                    runtime: 'automatic',
-                    development: true,
-                    refresh: true,
-                  },
-                },
-              },
-              env: { targets },
-            },
-          },
-        ],
-      },
-      {
-        test: /\.css$/,
-        use: [
-          'style-loader',
-          'css-loader',
-        ],
-      },
-    ],
-  },
-  optimization:{
-    runtimeChunk: 'single'
-  },
-  plugins: [
-    new ModuleFederationPlugin({
-      name: 'rspack',
-      runtime: false,
-      filename: 'remoteEntry.js',
-      exposes: {
-        './tjing': './src/App.jsx'
-      },
-      shared: {
-        react: {
-          singleton: true,
-        },
-        "react/jsx-dev-runtime": {
-          singleton: true,
-        },
-        "react/jsx-runtime": {
-          singleton: true,
-        },
-        "react-dom": {
-          singleton: true,
-        }
-      },
-    }),
-    new HtmlWebpackPlugin({
-      template: './index.html',
-      scriptLoading: 'module',
-      excludeChunks: ['rspack','app']
-    }),
-    new ReactRefreshWebpackPlugin(),
-  ],
-  devtool: false,
-  experiments: {
-    outputModule: true,
-  },
-  externalsType: 'module',
-  output: {
-    publicPath: 'auto',
-    chunkFormat: 'module',
-    chunkLoading: 'import',
-    workerChunkLoading: 'import',
-    wasmLoading: 'fetch',
-    library: { type: 'module' },
-    module: true,
-    filename: '[name].js',
-    chunkFilename: '[name].js',
-  },
-};
diff --git a/package.json b/package.json
index d455ea669a3..061785f6be7 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     }
   },
   "devDependencies": {
+    "@playwright/test": "^1.54.2",
     "@shelex/cypress-allure-plugin": "2.40.2",
     "@types/node": "20.9.0",
     "abbrev": "2.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a6780c60d88..81a65d36405 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,6 +13,9 @@ importers:
 
   .:
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.54.2
+        version: 1.54.2
       '@shelex/cypress-allure-plugin':
         specifier: 2.40.2
         version: 2.40.2
@@ -85,6 +88,9 @@ importers:
 
   advanced-api/automatic-vendor-sharing:
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.54.2
+        version: 1.54.2
       wait-on:
         specifier: 7.2.0
         version: 7.2.0
@@ -92,11 +98,11 @@ importers:
   advanced-api/automatic-vendor-sharing/app1:
     dependencies:
       react:
-        specifier: ^16.13.0
-        version: 16.14.0
+        specifier: ^18.3.1
+        version: 18.3.1
       react-dom:
-        specifier: ^16.13.0
-        version: 16.14.0(react@16.14.0)
+        specifier: ^18.3.1
+        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@babel/core':
         specifier: 7.24.7
@@ -104,9 +110,12 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
-        specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        specifier: ^0.17.1
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
       '@rspack/cli':
         specifier: 1.4.11
         version: 1.4.11(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
@@ -116,6 +125,12 @@ importers:
       '@rspack/dev-server':
         specifier: 1.1.3
         version: 1.1.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -125,6 +140,9 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      typescript:
+        specifier: ^5.5.4
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -138,11 +156,11 @@ importers:
   advanced-api/automatic-vendor-sharing/app2:
     dependencies:
       react:
-        specifier: ^16.13.0
-        version: 16.14.0
+        specifier: ^18.3.1
+        version: 18.3.1
       react-dom:
-        specifier: ^16.13.0
-        version: 16.14.0(react@16.14.0)
+        specifier: ^18.3.1
+        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@babel/core':
         specifier: 7.24.7
@@ -150,9 +168,12 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
-        specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        specifier: ^0.17.1
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
       '@rspack/cli':
         specifier: 1.4.11
         version: 1.4.11(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
@@ -162,6 +183,12 @@ importers:
       '@rspack/dev-server':
         specifier: 1.1.3
         version: 1.1.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -171,6 +198,9 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      typescript:
+        specifier: ^5.5.4
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -195,6 +225,9 @@ importers:
 
   advanced-api/dynamic-remotes-runtime-environment-variables:
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.54.2
+        version: 1.54.2
       wait-on:
         specifier: 7.2.0
         version: 7.2.0
@@ -202,14 +235,14 @@ importers:
   advanced-api/dynamic-remotes-runtime-environment-variables/host:
     dependencies:
       moment:
-        specifier: ^2.29.4
+        specifier: ^2.30.1
         version: 2.30.1
       react:
-        specifier: ^16.13.0
-        version: 16.14.0
+        specifier: ^18.3.1
+        version: 18.3.1
       react-dom:
-        specifier: ^16.13.0
-        version: 16.14.0(react@16.14.0)
+        specifier: ^18.3.1
+        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@babel/core':
         specifier: 7.24.7
@@ -219,7 +252,10 @@ importers:
         version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+      '@module-federation/retry-plugin':
+        specifier: 0.17.1
+        version: 0.17.1
       '@module-federation/runtime':
         specifier: 0.17.1
         version: 0.17.1
@@ -253,6 +289,9 @@ importers:
       webpack-dev-server:
         specifier: 5.0.4
         version: 5.0.4(webpack-cli@5.1.4)(webpack@5.101.0)
+      webpack-merge:
+        specifier: 6.0.1
+        version: 6.0.1
 
   advanced-api/dynamic-remotes-runtime-environment-variables/remote:
     dependencies:
@@ -260,14 +299,14 @@ importers:
         specifier: 12.0.2
         version: 12.0.2(webpack@5.101.0)
       moment:
-        specifier: ^2.29.4
+        specifier: ^2.30.1
         version: 2.30.1
       react:
-        specifier: ^16.13.0
-        version: 16.14.0
+        specifier: ^18.3.1
+        version: 18.3.1
       react-dom:
-        specifier: ^16.13.0
-        version: 16.14.0(react@16.14.0)
+        specifier: ^18.3.1
+        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@babel/core':
         specifier: 7.24.7
@@ -277,7 +316,10 @@ importers:
         version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+      '@module-federation/retry-plugin':
+        specifier: 0.17.1
+        version: 0.17.1
       '@rspack/cli':
         specifier: 1.4.11
         version: 1.4.11(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
@@ -305,9 +347,15 @@ importers:
       webpack-dev-server:
         specifier: 5.0.4
         version: 5.0.4(webpack-cli@5.1.4)(webpack@5.101.0)
+      webpack-merge:
+        specifier: 6.0.1
+        version: 6.0.1
 
   advanced-api/dynamic-remotes-synchronous-imports:
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.54.2
+        version: 1.54.2
       wait-on:
         specifier: 7.2.0
         version: 7.2.0
@@ -318,11 +366,11 @@ importers:
         specifier: ^2.29.4
         version: 2.30.1
       react:
-        specifier: ^16.13.0
-        version: 16.14.0
+        specifier: ^18.3.1
+        version: 18.3.1
       react-dom:
-        specifier: ^16.13.0
-        version: 16.14.0(react@16.14.0)
+        specifier: ^18.3.1
+        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@babel/core':
         specifier: 7.24.7
@@ -330,9 +378,18 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -342,6 +399,9 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      typescript:
+        specifier: ^5.5.3
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -358,14 +418,14 @@ importers:
         specifier: ^2.29.4
         version: 2.30.1
       react:
-        specifier: ^16.13.0
-        version: 16.14.0
+        specifier: ^18.3.1
+        version: 18.3.1
       react-dom:
-        specifier: ^16.13.0
-        version: 16.14.0(react@16.14.0)
+        specifier: ^18.3.1
+        version: 18.3.1(react@18.3.1)
       react-redux:
         specifier: ^7.2.0
-        version: 7.2.9(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
+        version: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       redux:
         specifier: ^4.2.1
         version: 4.2.1
@@ -376,9 +436,18 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -388,6 +457,9 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      typescript:
+        specifier: ^5.5.3
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -413,9 +485,12 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
       '@module-federation/runtime':
         specifier: 0.17.1
         version: 0.17.1
@@ -428,6 +503,12 @@ importers:
       '@rspack/dev-server':
         specifier: 1.1.3
         version: 1.1.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -437,6 +518,12 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      ts-loader:
+        specifier: ^9.5.1
+        version: 9.5.1(typescript@5.6.2)(webpack@5.101.0)
+      typescript:
+        specifier: ^5.5.4
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -465,9 +552,12 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
       '@rspack/cli':
         specifier: 1.4.11
         version: 1.4.11(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
@@ -477,6 +567,12 @@ importers:
       '@rspack/dev-server':
         specifier: 1.1.3
         version: 1.1.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -486,6 +582,12 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      ts-loader:
+        specifier: ^9.5.1
+        version: 9.5.1(typescript@5.6.2)(webpack@5.101.0)
+      typescript:
+        specifier: ^5.5.4
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -520,9 +622,12 @@ importers:
       '@babel/preset-react':
         specifier: 7.24.7
         version: 7.24.7(@babel/core@7.24.7)
+      '@babel/preset-typescript':
+        specifier: ^7.24.7
+        version: 7.24.7(@babel/core@7.24.7)
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0)
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)
       '@rspack/cli':
         specifier: 1.4.11
         version: 1.4.11(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
@@ -532,6 +637,12 @@ importers:
       '@rspack/dev-server':
         specifier: 1.1.3
         version: 1.1.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@types/express@4.17.21)(webpack-cli@5.1.4)(webpack@5.101.0)
+      '@types/react':
+        specifier: 18.3.10
+        version: 18.3.10
+      '@types/react-dom':
+        specifier: 18.3.0
+        version: 18.3.0
       babel-loader:
         specifier: 9.1.3
         version: 9.1.3(@babel/core@7.24.7)(webpack@5.101.0)
@@ -541,6 +652,12 @@ importers:
       serve:
         specifier: 14.2.3
         version: 14.2.3
+      ts-loader:
+        specifier: ^9.5.1
+        version: 9.5.1(typescript@5.6.2)(webpack@5.101.0)
+      typescript:
+        specifier: ^5.5.4
+        version: 5.6.2
       webpack:
         specifier: 5.101.0
         version: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
@@ -874,7 +991,7 @@ importers:
         version: 2.68.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@module-federation/modern-js':
         specifier: 0.17.1
-        version: 0.17.1(@rsbuild/core@1.4.11)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       react:
         specifier: ~18.3.0
         version: 18.3.1
@@ -887,10 +1004,10 @@ importers:
         version: 2.59.0(typescript@5.9.2)
       '@modern-js/app-tools':
         specifier: 2.68.6
-        version: 2.68.6(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@5.9.2)(webpack-dev-server@5.2.2(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)))(webpack-hot-middleware@2.26.1)
+        version: 2.68.6(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.1.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@5.9.2)(webpack-dev-server@5.2.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))))(webpack-hot-middleware@2.26.1)
       '@modern-js/builder-rspack-provider':
         specifier: 2.46.1
-        version: 2.46.1(@babel/traverse@7.28.0)(@rsbuild/core@1.4.11)(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/express@4.17.21)(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(typescript@5.9.2)
+        version: 2.46.1(@babel/traverse@7.28.0)(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(typescript@5.9.2)
       '@modern-js/eslint-config':
         specifier: 2.59.0
         version: 2.59.0(typescript@5.9.2)
@@ -899,7 +1016,7 @@ importers:
         version: 2.68.6
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       husky:
         specifier: 9.0.11
         version: 9.0.11
@@ -920,7 +1037,7 @@ importers:
         version: 2.68.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@module-federation/modern-js':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+        version: 0.17.1(@rsbuild/core@1.4.11)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
       react:
         specifier: ~18.3.0
         version: 18.3.1
@@ -933,10 +1050,10 @@ importers:
         version: 2.59.0(typescript@5.9.2)
       '@modern-js/app-tools':
         specifier: 2.68.6
-        version: 2.68.6(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.1.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@5.9.2)(webpack-dev-server@5.2.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))))(webpack-hot-middleware@2.26.1)
+        version: 2.68.6(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@5.9.2)(webpack-dev-server@5.2.2(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)))(webpack-hot-middleware@2.26.1)
       '@modern-js/builder-rspack-provider':
         specifier: 2.46.1
-        version: 2.46.1(@babel/traverse@7.28.0)(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(typescript@5.9.2)
+        version: 2.46.1(@babel/traverse@7.28.0)(@rsbuild/core@1.4.11)(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/express@4.17.21)(esbuild@0.25.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@22.17.0)(typescript@5.9.2))(tsconfig-paths@4.2.0)(typescript@5.9.2)
       '@modern-js/eslint-config':
         specifier: 2.59.0
         version: 2.59.0(typescript@5.9.2)
@@ -945,7 +1062,7 @@ importers:
         version: 2.68.6
       '@module-federation/enhanced':
         specifier: 0.17.1
-        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+        version: 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
       husky:
         specifier: 9.0.11
         version: 9.0.11
@@ -5758,7 +5875,7 @@ importers:
         version: 2.57.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@module-federation/modern-js':
         specifier: 0.17.1
-        version: 0.17.1(@rsbuild/core@1.4.12)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@4.9.5)(vue-tsc@1.8.27(typescript@4.9.5))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+        version: 0.17.1(@rsbuild/core@1.0.1-beta.3)(@rspack/core@1.4.11(@swc/helpers@0.5.11))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@4.9.5)(vue-tsc@1.8.27(typescript@4.9.5))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))
       react:
         specifier: ~18.3.0
         version: 18.3.1
@@ -5771,7 +5888,7 @@ importers:
         version: 2.57.0(typescript@4.9.5)
       '@modern-js/app-tools':
         specifier: 2.57.0
-        version: 2.57.0(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))(encoding@0.1.13)(eslint@9.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.1.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@16.18.101)(typescript@4.9.5))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@4.9.5)(webpack-dev-server@4.15.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))))(webpack-hot-middleware@2.26.1)
+        version: 2.57.0(@rspack/core@1.4.11(@swc/helpers@0.5.11))(@swc/core@1.13.3(@swc/helpers@0.5.11))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.11))(@swc/wasm@1.13.3)(@types/node@16.18.101)(typescript@4.9.5))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@4.9.5)(webpack-dev-server@4.15.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19)))(webpack-hot-middleware@2.26.1)
       '@modern-js/eslint-config':
         specifier: 2.57.0
         version: 2.57.0(typescript@4.9.5)
@@ -5813,7 +5930,7 @@ importers:
         version: 2.57.0(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@module-federation/modern-js':
         specifier: 0.17.1
-        version: 0.17.1(@rsbuild/core@1.0.1-beta.3)(@rspack/core@1.4.11(@swc/helpers@0.5.11))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@4.9.5)(vue-tsc@1.8.27(typescript@4.9.5))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))
+        version: 0.17.1(@rsbuild/core@1.4.12)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(typescript@4.9.5)(vue-tsc@1.8.27(typescript@4.9.5))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -5826,7 +5943,7 @@ importers:
         version: 2.57.0(typescript@4.9.5)
       '@modern-js/app-tools':
         specifier: 2.57.0
-        version: 2.57.0(@rspack/core@1.4.11(@swc/helpers@0.5.11))(@swc/core@1.13.3(@swc/helpers@0.5.11))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))(encoding@0.1.13)(eslint@8.57.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.11))(@swc/wasm@1.13.3)(@types/node@16.18.101)(typescript@4.9.5))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@4.9.5)(webpack-dev-server@4.15.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19)))(webpack-hot-middleware@2.26.1)
+        version: 2.57.0(@rspack/core@1.4.11(@swc/helpers@0.5.17))(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/webpack@5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))(encoding@0.1.13)(eslint@9.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.28.0)(react-dom@18.3.1(react@18.3.1))(react-is@19.1.1)(react@18.3.1))(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/wasm@1.13.3)(@types/node@16.18.101)(typescript@4.9.5))(tsconfig-paths@4.2.0)(type-fest@2.19.0)(typescript@4.9.5)(webpack-dev-server@4.15.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))))(webpack-hot-middleware@2.26.1)
       '@modern-js/eslint-config':
         specifier: 2.57.0
         version: 2.57.0(typescript@4.9.5)
@@ -6767,7 +6884,7 @@ importers:
     devDependencies:
       lerna:
         specifier: 8.1.6
-        version: 8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)
+        version: 8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)
       wait-on:
         specifier: 7.2.0
         version: 7.2.0
@@ -12328,6 +12445,9 @@ packages:
       react-dom:
         optional: true
 
+  '@module-federation/retry-plugin@0.17.1':
+    resolution: {integrity: sha512-lI/y0C5PbABiD31I0LYHTN3CkHDXj9uba/wgZ2IiiKi6wY0VWLpZ/A+xn79hGS0ac8ANp2MHofpgIWSRxBl/XA==}
+
   '@module-federation/rsbuild-plugin@0.17.1':
     resolution: {integrity: sha512-+Wd6X34A7cMDPNj59EdQyFnx9tdbjSJL3+a1oIqq8WGA52+1uACNCkEzXxQW/Q1eoFbtZfk/jduDmz97rx3c2A==}
     engines: {node: '>=16.0.0'}
@@ -28085,7 +28205,7 @@ snapshots:
       eslint-visitor-keys: 2.1.0
       semver: 6.3.1
 
-  '@babel/eslint-plugin@7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.28.0)(eslint@8.57.1))(eslint@8.57.1)':
+  '@babel/eslint-plugin@7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.24.7)(eslint@8.57.1))(eslint@8.57.1)':
     dependencies:
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@8.57.1)
       eslint: 8.57.1
@@ -30126,7 +30246,7 @@ snapshots:
       '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.22.17)
       '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.22.17)
       '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.22.17)
-      '@babel/plugin-transform-modules-commonjs': 7.22.15(@babel/core@7.22.17)
+      '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.22.17)
       '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.22.17)
       '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.22.17)
       '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.22.17)
@@ -32684,12 +32804,12 @@ snapshots:
 
   '@leichtgewicht/ip-codec@2.0.5': {}
 
-  '@lerna/create@8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.9.2)':
+  '@lerna/create@8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.6.2)':
     dependencies:
       '@npmcli/arborist': 7.5.3
       '@npmcli/package-json': 5.2.0
       '@npmcli/run-script': 8.1.0
-      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       '@octokit/plugin-enterprise-rest': 6.0.1
       '@octokit/rest': 19.0.11(encoding@0.1.13)
       aproba: 2.0.0
@@ -32702,7 +32822,89 @@ snapshots:
       console-control-strings: 1.1.0
       conventional-changelog-core: 5.0.1
       conventional-recommended-bump: 7.0.1
-      cosmiconfig: 8.3.6(typescript@5.9.2)
+      cosmiconfig: 8.3.6(typescript@5.6.2)
+      dedent: 1.5.3(babel-plugin-macros@3.1.0)
+      execa: 5.0.0
+      fs-extra: 11.3.0
+      get-stream: 6.0.0
+      git-url-parse: 14.0.0
+      glob-parent: 6.0.2
+      globby: 11.1.0
+      graceful-fs: 4.2.11
+      has-unicode: 2.0.1
+      ini: 1.3.8
+      init-package-json: 6.0.3
+      inquirer: 8.2.6
+      is-ci: 3.0.1
+      is-stream: 2.0.0
+      js-yaml: 4.1.0
+      libnpmpublish: 9.0.9
+      load-json-file: 6.2.0
+      lodash: 4.17.21
+      make-dir: 4.0.0
+      minimatch: 3.0.5
+      multimatch: 5.0.0
+      node-fetch: 2.6.7(encoding@0.1.13)
+      npm-package-arg: 11.0.2
+      npm-packlist: 8.0.2
+      npm-registry-fetch: 17.1.0
+      nx: 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
+      p-map: 4.0.0
+      p-map-series: 2.1.0
+      p-queue: 6.6.2
+      p-reduce: 2.1.0
+      pacote: 18.0.6
+      pify: 5.0.0
+      read-cmd-shim: 4.0.0
+      resolve-from: 5.0.0
+      rimraf: 4.4.1
+      semver: 7.6.3
+      set-blocking: 2.0.0
+      signal-exit: 3.0.7
+      slash: 3.0.0
+      ssri: 10.0.6
+      string-width: 4.2.3
+      strong-log-transformer: 2.1.0
+      tar: 6.2.1
+      temp-dir: 1.0.0
+      upath: 2.0.1
+      uuid: 10.0.0
+      validate-npm-package-license: 3.0.4
+      validate-npm-package-name: 5.0.1
+      wide-align: 1.1.5
+      write-file-atomic: 5.0.1
+      write-pkg: 4.0.0
+      yargs: 17.7.2
+      yargs-parser: 21.1.1
+    transitivePeerDependencies:
+      - '@swc-node/register'
+      - '@swc/core'
+      - babel-plugin-macros
+      - bluebird
+      - debug
+      - encoding
+      - supports-color
+      - typescript
+
+  '@lerna/create@8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.6.2)':
+    dependencies:
+      '@npmcli/arborist': 7.5.3
+      '@npmcli/package-json': 5.2.0
+      '@npmcli/run-script': 8.1.0
+      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      '@octokit/plugin-enterprise-rest': 6.0.1
+      '@octokit/rest': 19.0.11(encoding@0.1.13)
+      aproba: 2.0.0
+      byte-size: 8.1.1
+      chalk: 4.1.0
+      clone-deep: 4.0.1
+      cmd-shim: 6.0.3
+      color-support: 1.1.3
+      columnify: 1.6.0
+      console-control-strings: 1.1.0
+      conventional-changelog-core: 5.0.1
+      conventional-recommended-bump: 7.0.1
+      cosmiconfig: 8.3.6(typescript@5.6.2)
       dedent: 1.5.3(babel-plugin-macros@3.1.0)
       execa: 5.0.0
       fs-extra: 11.3.0
@@ -32766,7 +32968,7 @@ snapshots:
       - supports-color
       - typescript
 
-  '@lerna/create@8.1.8(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.9.2)':
+  '@lerna/create@8.1.8(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.6.2)':
     dependencies:
       '@npmcli/arborist': 7.5.4
       '@npmcli/package-json': 5.2.0
@@ -32784,7 +32986,7 @@ snapshots:
       console-control-strings: 1.1.0
       conventional-changelog-core: 5.0.1
       conventional-recommended-bump: 7.0.1
-      cosmiconfig: 8.3.6(typescript@5.9.2)
+      cosmiconfig: 8.3.6(typescript@5.6.2)
       dedent: 1.5.3(babel-plugin-macros@3.1.0)
       execa: 5.0.0
       fs-extra: 11.3.0
@@ -33100,7 +33302,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.7
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@8.57.1)
-      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.28.0)(eslint@8.57.1))(eslint@8.57.1)
+      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.24.7)(eslint@8.57.1))(eslint@8.57.1)
       '@rsbuild/babel-preset': 0.7.10(@rsbuild/core@0.7.10)(@swc/helpers@0.5.17)
       '@rsbuild/core': 0.7.10
       '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.3))(eslint@8.57.1)(typescript@5.5.3)
@@ -33127,7 +33329,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.7
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@8.57.1)
-      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.28.0)(eslint@8.57.1))(eslint@8.57.1)
+      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.24.7)(eslint@8.57.1))(eslint@8.57.1)
       '@rsbuild/babel-preset': 0.7.10(@rsbuild/core@0.7.10)(@swc/helpers@0.5.17)
       '@rsbuild/core': 0.7.10
       '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
@@ -33154,7 +33356,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.7
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@8.57.1)
-      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.28.0)(eslint@8.57.1))(eslint@8.57.1)
+      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.24.7)(eslint@8.57.1))(eslint@8.57.1)
       '@rsbuild/babel-preset': 0.7.10(@rsbuild/core@0.7.10)(@swc/helpers@0.5.3)
       '@rsbuild/core': 0.7.10
       '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
@@ -33181,7 +33383,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.7
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@8.57.1)
-      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.28.0)(eslint@8.57.1))(eslint@8.57.1)
+      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.24.7)(eslint@8.57.1))(eslint@8.57.1)
       '@modern-js/babel-preset': 2.57.0(@rsbuild/core@1.0.1-beta.3)
       '@rsbuild/core': 1.0.1-beta.3
       '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
@@ -33207,7 +33409,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.24.7
       '@babel/eslint-parser': 7.24.7(@babel/core@7.24.7)(eslint@8.57.1)
-      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.28.0)(eslint@8.57.1))(eslint@8.57.1)
+      '@babel/eslint-plugin': 7.27.1(@babel/eslint-parser@7.24.7(@babel/core@7.24.7)(eslint@8.57.1))(eslint@8.57.1)
       '@modern-js/babel-preset': 2.59.0(@rsbuild/core@1.0.1-rc.4)
       '@rsbuild/core': 1.0.1-rc.4
       '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
@@ -33881,7 +34083,7 @@ snapshots:
       react-refresh: 0.14.2
       rspack-manifest-plugin: 5.0.0-alpha0(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
       style-loader: 3.3.3(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     transitivePeerDependencies:
       - '@babel/traverse'
       - '@parcel/css'
@@ -34101,7 +34303,7 @@ snapshots:
       line-diff: 2.1.1
       postcss: 8.4.31
       source-map: 0.7.4
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
       webpack-sources: 3.2.3
       zod: 3.25.76
       zod-validation-error: 1.2.0(zod@3.25.76)
@@ -34597,7 +34799,7 @@ snapshots:
     dependencies:
       '@swc/helpers': 0.5.17
       esbuild: 0.25.5
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     transitivePeerDependencies:
       - '@swc/core'
       - uglify-js
@@ -35091,7 +35293,7 @@ snapshots:
       '@babel/register': 7.27.1(@babel/core@7.24.7)
       '@modern-js/prod-server': 2.46.1(@types/express@4.17.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@modern-js/runtime-utils': 2.46.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      '@modern-js/server-utils': 2.46.1(@babel/traverse@7.28.0)(@rsbuild/core@1.4.12)
+      '@modern-js/server-utils': 2.46.1(@babel/traverse@7.28.0)(@rsbuild/core@1.4.11)
       '@modern-js/types': 2.46.1
       '@modern-js/utils': 2.46.1
       '@swc/helpers': 0.5.3
@@ -35754,7 +35956,7 @@ snapshots:
       terser-webpack-plugin: 5.3.14(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
       ts-deepmerge: 7.0.2
       ts-loader: 9.4.4(typescript@5.9.2)(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
       webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
     transitivePeerDependencies:
       - '@parcel/css'
@@ -35833,7 +36035,7 @@ snapshots:
       terser-webpack-plugin: 5.3.14(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       ts-deepmerge: 7.0.2
       ts-loader: 9.4.4(typescript@5.9.2)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
       webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
     transitivePeerDependencies:
       - '@parcel/css'
@@ -36410,6 +36612,35 @@ snapshots:
       - supports-color
       - utf-8-validate
 
+  '@module-federation/enhanced@0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))(webpack@5.101.0)':
+    dependencies:
+      '@module-federation/bridge-react-webpack-plugin': 0.17.1
+      '@module-federation/cli': 0.17.1(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))
+      '@module-federation/data-prefetch': 0.17.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@module-federation/dts-plugin': 0.17.1(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))
+      '@module-federation/error-codes': 0.17.1
+      '@module-federation/inject-external-runtime-core-plugin': 0.17.1(@module-federation/runtime-tools@0.17.1)
+      '@module-federation/managers': 0.17.1
+      '@module-federation/manifest': 0.17.1(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))
+      '@module-federation/rspack': 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(typescript@5.6.2)(vue-tsc@1.8.27(typescript@5.6.2))
+      '@module-federation/runtime-tools': 0.17.1
+      '@module-federation/sdk': 0.17.1
+      btoa: 1.2.1
+      schema-utils: 4.3.2
+      upath: 2.0.1
+    optionalDependencies:
+      typescript: 5.6.2
+      vue-tsc: 1.8.27(typescript@5.6.2)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(webpack-cli@5.1.4)
+    transitivePeerDependencies:
+      - '@rspack/core'
+      - bufferutil
+      - debug
+      - react
+      - react-dom
+      - supports-color
+      - utf-8-validate
+
   '@module-federation/enhanced@0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2)(vue-tsc@1.8.27(typescript@5.9.2))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))':
     dependencies:
       '@module-federation/bridge-react-webpack-plugin': 0.17.1
@@ -36429,7 +36660,7 @@ snapshots:
     optionalDependencies:
       typescript: 5.9.2
       vue-tsc: 1.8.27(typescript@5.9.2)
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     transitivePeerDependencies:
       - '@rspack/core'
       - bufferutil
@@ -37098,7 +37329,7 @@ snapshots:
       btoa: 1.2.1
       encoding: 0.1.13
       node-fetch: 2.7.0(encoding@0.1.13)
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     optionalDependencies:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
@@ -37240,6 +37471,10 @@ snapshots:
       - utf-8-validate
       - vue-tsc
 
+  '@module-federation/retry-plugin@0.17.1':
+    dependencies:
+      '@module-federation/sdk': 0.17.1
+
   '@module-federation/rsbuild-plugin@0.17.1(@rsbuild/core@1.0.1-beta.3)(@rspack/core@1.4.11(@swc/helpers@0.5.11))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@4.9.5)(vue-tsc@1.8.27(typescript@4.9.5))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))':
     dependencies:
       '@module-federation/enhanced': 0.17.1(@rspack/core@1.4.11(@swc/helpers@0.5.11))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@4.9.5)(vue-tsc@1.8.27(typescript@4.9.5))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))
@@ -38019,9 +38254,9 @@ snapshots:
     transitivePeerDependencies:
       - nx
 
-  '@nrwl/devkit@19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))':
+  '@nrwl/devkit@19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))':
     dependencies:
-      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
     transitivePeerDependencies:
       - nx
 
@@ -38184,6 +38419,15 @@ snapshots:
       - '@swc/core'
       - debug
 
+  '@nrwl/tao@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))':
+    dependencies:
+      nx: 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@swc-node/register'
+      - '@swc/core'
+      - debug
+
   '@nrwl/tao@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))':
     dependencies:
       nx: 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
@@ -38332,14 +38576,14 @@ snapshots:
       tslib: 2.8.1
       yargs-parser: 21.1.1
 
-  '@nx/devkit@19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))':
+  '@nx/devkit@19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))':
     dependencies:
-      '@nrwl/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      '@nrwl/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       ejs: 3.1.10
       enquirer: 2.3.6
       ignore: 5.3.2
       minimatch: 9.0.3
-      nx: 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
+      nx: 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
       semver: 7.6.3
       tmp: 0.2.3
       tslib: 2.8.1
@@ -39179,7 +39423,7 @@ snapshots:
       react-refresh: 0.14.2
       schema-utils: 4.3.2
       source-map: 0.7.6
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     optionalDependencies:
       '@types/webpack': 5.28.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
       type-fest: 2.19.0
@@ -40422,7 +40666,7 @@ snapshots:
       picocolors: 1.1.1
       reduce-configs: 1.1.0
       tsconfig-paths-webpack-plugin: 4.2.0
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     transitivePeerDependencies:
       - '@rspack/core'
       - '@swc/core'
@@ -41801,6 +42045,22 @@ snapshots:
       - supports-color
     optional: true
 
+  '@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2)':
+    dependencies:
+      '@swc-node/core': 1.13.3(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)
+      '@swc-node/sourcemap-support': 0.5.1
+      '@swc/core': 1.13.3(@swc/helpers@0.5.17)
+      colorette: 2.0.20
+      debug: 4.4.1(supports-color@8.1.1)
+      oxc-resolver: 1.12.0
+      pirates: 4.0.7
+      tslib: 2.8.1
+      typescript: 5.6.2
+    transitivePeerDependencies:
+      - '@swc/types'
+      - supports-color
+    optional: true
+
   '@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2)':
     dependencies:
       '@swc-node/core': 1.13.3(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)
@@ -42461,7 +42721,7 @@ snapshots:
     dependencies:
       '@types/node': 18.19.39
       tapable: 2.2.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     transitivePeerDependencies:
       - '@swc/core'
       - esbuild
@@ -44862,7 +45122,7 @@ snapshots:
       '@babel/core': 7.28.0
       find-cache-dir: 4.0.0
       schema-utils: 4.3.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))):
     dependencies:
@@ -46269,7 +46529,7 @@ snapshots:
       normalize-path: 3.0.0
       schema-utils: 4.3.2
       serialize-javascript: 6.0.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   copy-webpack-plugin@11.0.0(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19)):
     dependencies:
@@ -46395,6 +46655,15 @@ snapshots:
     optionalDependencies:
       typescript: 5.5.3
 
+  cosmiconfig@8.3.6(typescript@5.6.2):
+    dependencies:
+      import-fresh: 3.3.1
+      js-yaml: 4.1.0
+      parse-json: 5.2.0
+      path-type: 4.0.0
+    optionalDependencies:
+      typescript: 5.6.2
+
   cosmiconfig@8.3.6(typescript@5.9.2):
     dependencies:
       import-fresh: 3.3.1
@@ -46758,7 +47027,7 @@ snapshots:
       postcss: 8.4.47
       schema-utils: 4.3.2
       serialize-javascript: 6.0.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     optionalDependencies:
       esbuild: 0.25.5
 
@@ -49701,7 +49970,7 @@ snapshots:
       semver: 7.6.3
       tapable: 2.2.2
       typescript: 5.9.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   fork-ts-checker-webpack-plugin@8.0.0(typescript@5.9.2)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)):
     dependencies:
@@ -50525,7 +50794,7 @@ snapshots:
       tapable: 2.2.2
     optionalDependencies:
       '@rspack/core': 1.4.11(@swc/helpers@0.5.17)
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   html-webpack-plugin@5.6.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)):
     dependencies:
@@ -52537,13 +52806,104 @@ snapshots:
 
   lazy@1.0.11: {}
 
+  lerna@8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13):
+    dependencies:
+      '@lerna/create': 8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.6.2)
+      '@npmcli/arborist': 7.5.3
+      '@npmcli/package-json': 5.2.0
+      '@npmcli/run-script': 8.1.0
+      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      '@octokit/plugin-enterprise-rest': 6.0.1
+      '@octokit/rest': 19.0.11(encoding@0.1.13)
+      aproba: 2.0.0
+      byte-size: 8.1.1
+      chalk: 4.1.0
+      clone-deep: 4.0.1
+      cmd-shim: 6.0.3
+      color-support: 1.1.3
+      columnify: 1.6.0
+      console-control-strings: 1.1.0
+      conventional-changelog-angular: 7.0.0
+      conventional-changelog-core: 5.0.1
+      conventional-recommended-bump: 7.0.1
+      cosmiconfig: 8.3.6(typescript@5.6.2)
+      dedent: 1.5.3(babel-plugin-macros@3.1.0)
+      envinfo: 7.13.0
+      execa: 5.0.0
+      fs-extra: 11.3.0
+      get-port: 5.1.1
+      get-stream: 6.0.0
+      git-url-parse: 14.0.0
+      glob-parent: 6.0.2
+      globby: 11.1.0
+      graceful-fs: 4.2.11
+      has-unicode: 2.0.1
+      import-local: 3.1.0
+      ini: 1.3.8
+      init-package-json: 6.0.3
+      inquirer: 8.2.6
+      is-ci: 3.0.1
+      is-stream: 2.0.0
+      jest-diff: 29.7.0
+      js-yaml: 4.1.0
+      libnpmaccess: 8.0.6
+      libnpmpublish: 9.0.9
+      load-json-file: 6.2.0
+      lodash: 4.17.21
+      make-dir: 4.0.0
+      minimatch: 3.0.5
+      multimatch: 5.0.0
+      node-fetch: 2.6.7(encoding@0.1.13)
+      npm-package-arg: 11.0.2
+      npm-packlist: 8.0.2
+      npm-registry-fetch: 17.1.0
+      nx: 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
+      p-map: 4.0.0
+      p-map-series: 2.1.0
+      p-pipe: 3.1.0
+      p-queue: 6.6.2
+      p-reduce: 2.1.0
+      p-waterfall: 2.1.1
+      pacote: 18.0.6
+      pify: 5.0.0
+      read-cmd-shim: 4.0.0
+      resolve-from: 5.0.0
+      rimraf: 4.4.1
+      semver: 7.6.3
+      set-blocking: 2.0.0
+      signal-exit: 3.0.7
+      slash: 3.0.0
+      ssri: 10.0.6
+      string-width: 4.2.3
+      strong-log-transformer: 2.1.0
+      tar: 6.2.1
+      temp-dir: 1.0.0
+      typescript: 5.6.2
+      upath: 2.0.1
+      uuid: 10.0.0
+      validate-npm-package-license: 3.0.4
+      validate-npm-package-name: 5.0.1
+      wide-align: 1.1.5
+      write-file-atomic: 5.0.1
+      write-pkg: 4.0.0
+      yargs: 17.7.2
+      yargs-parser: 21.1.1
+    transitivePeerDependencies:
+      - '@swc-node/register'
+      - '@swc/core'
+      - babel-plugin-macros
+      - bluebird
+      - debug
+      - encoding
+      - supports-color
+
   lerna@8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13):
     dependencies:
-      '@lerna/create': 8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.9.2)
+      '@lerna/create': 8.1.6(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.6.2)
       '@npmcli/arborist': 7.5.3
       '@npmcli/package-json': 5.2.0
       '@npmcli/run-script': 8.1.0
-      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      '@nx/devkit': 19.8.14(nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       '@octokit/plugin-enterprise-rest': 6.0.1
       '@octokit/rest': 19.0.11(encoding@0.1.13)
       aproba: 2.0.0
@@ -52557,7 +52917,7 @@ snapshots:
       conventional-changelog-angular: 7.0.0
       conventional-changelog-core: 5.0.1
       conventional-recommended-bump: 7.0.1
-      cosmiconfig: 8.3.6(typescript@5.9.2)
+      cosmiconfig: 8.3.6(typescript@5.6.2)
       dedent: 1.5.3(babel-plugin-macros@3.1.0)
       envinfo: 7.13.0
       execa: 5.0.0
@@ -52609,7 +52969,7 @@ snapshots:
       strong-log-transformer: 2.1.0
       tar: 6.2.1
       temp-dir: 1.0.0
-      typescript: 5.9.2
+      typescript: 5.6.2
       upath: 2.0.1
       uuid: 10.0.0
       validate-npm-package-license: 3.0.4
@@ -52630,7 +52990,7 @@ snapshots:
 
   lerna@8.1.8(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13):
     dependencies:
-      '@lerna/create': 8.1.8(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.9.2)
+      '@lerna/create': 8.1.8(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.5.3))(@swc/core@1.13.3(@swc/helpers@0.5.17))(babel-plugin-macros@3.1.0)(encoding@0.1.13)(typescript@5.6.2)
       '@npmcli/arborist': 7.5.4
       '@npmcli/package-json': 5.2.0
       '@npmcli/run-script': 8.1.0
@@ -52648,7 +53008,7 @@ snapshots:
       conventional-changelog-angular: 7.0.0
       conventional-changelog-core: 5.0.1
       conventional-recommended-bump: 7.0.1
-      cosmiconfig: 8.3.6(typescript@5.9.2)
+      cosmiconfig: 8.3.6(typescript@5.6.2)
       dedent: 1.5.3(babel-plugin-macros@3.1.0)
       envinfo: 7.13.0
       execa: 5.0.0
@@ -52701,7 +53061,7 @@ snapshots:
       strong-log-transformer: 2.1.0
       tar: 6.2.1
       temp-dir: 1.0.0
-      typescript: 5.9.2
+      typescript: 5.6.2
       upath: 2.0.1
       uuid: 10.0.0
       validate-npm-package-license: 3.0.4
@@ -53641,7 +54001,7 @@ snapshots:
     dependencies:
       schema-utils: 4.3.2
       tapable: 2.2.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   mini-css-extract-plugin@2.9.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)):
     dependencies:
@@ -54239,6 +54599,58 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
+  nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)):
+    dependencies:
+      '@nrwl/tao': 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
+      '@yarnpkg/lockfile': 1.1.0
+      '@yarnpkg/parsers': 3.0.0-rc.46
+      '@zkochan/js-yaml': 0.0.6
+      axios: 1.7.7
+      chalk: 4.1.2
+      cli-cursor: 3.1.0
+      cli-spinners: 2.6.1
+      cliui: 8.0.1
+      dotenv: 16.3.2
+      dotenv-expand: 10.0.0
+      enquirer: 2.3.6
+      figures: 3.2.0
+      flat: 5.0.2
+      fs-extra: 11.3.0
+      ignore: 5.3.2
+      jest-diff: 29.7.0
+      js-yaml: 4.1.0
+      jsonc-parser: 3.2.0
+      lines-and-columns: 2.0.4
+      minimatch: 9.0.3
+      node-machine-id: 1.1.12
+      npm-run-path: 4.0.1
+      open: 8.4.2
+      ora: 5.3.0
+      semver: 7.6.3
+      string-width: 4.2.3
+      strong-log-transformer: 2.1.0
+      tar-stream: 2.2.0
+      tmp: 0.2.3
+      tsconfig-paths: 4.2.0
+      tslib: 2.8.1
+      yargs: 17.7.2
+      yargs-parser: 21.1.1
+    optionalDependencies:
+      '@nx/nx-darwin-arm64': 17.3.2
+      '@nx/nx-darwin-x64': 17.3.2
+      '@nx/nx-freebsd-x64': 17.3.2
+      '@nx/nx-linux-arm-gnueabihf': 17.3.2
+      '@nx/nx-linux-arm64-gnu': 17.3.2
+      '@nx/nx-linux-arm64-musl': 17.3.2
+      '@nx/nx-linux-x64-gnu': 17.3.2
+      '@nx/nx-linux-x64-musl': 17.3.2
+      '@nx/nx-win32-arm64-msvc': 17.3.2
+      '@nx/nx-win32-x64-msvc': 17.3.2
+      '@swc-node/register': 1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.6.2)
+      '@swc/core': 1.13.3(@swc/helpers@0.5.17)
+    transitivePeerDependencies:
+      - debug
+
   nx@17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17)):
     dependencies:
       '@nrwl/tao': 17.3.2(@swc-node/register@1.10.9(@swc/core@1.13.3(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.2))(@swc/core@1.13.3(@swc/helpers@0.5.17))
@@ -57244,6 +57656,18 @@ snapshots:
     optionalDependencies:
       react-dom: 16.14.0(react@16.14.0)
 
+  react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+    dependencies:
+      '@babel/runtime': 7.28.2
+      '@types/react-redux': 7.1.34
+      hoist-non-react-statics: 3.3.2
+      loose-envify: 1.4.0
+      prop-types: 15.8.1
+      react: 18.3.1
+      react-is: 17.0.2
+    optionalDependencies:
+      react-dom: 18.3.1(react@18.3.1)
+
   react-redux@9.2.0(@types/react@18.3.10)(react@18.3.1)(redux@5.0.1):
     dependencies:
       '@types/use-sync-external-store': 0.0.6
@@ -58114,7 +58538,7 @@ snapshots:
   rspack-manifest-plugin@5.0.0-alpha0(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)):
     dependencies:
       tapable: 2.2.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
       webpack-sources: 2.3.1
 
   rspack-manifest-plugin@5.0.0-alpha0(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))):
@@ -59119,7 +59543,7 @@ snapshots:
 
   style-loader@3.3.3(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)):
     dependencies:
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   style-loader@3.3.3(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))):
     dependencies:
@@ -59635,7 +60059,7 @@ snapshots:
       schema-utils: 4.3.2
       serialize-javascript: 6.0.2
       terser: 5.43.1
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     optionalDependencies:
       '@swc/core': 1.11.31(@swc/helpers@0.5.17)
       esbuild: 0.25.5
@@ -60038,7 +60462,7 @@ snapshots:
       micromatch: 4.0.8
       semver: 7.6.3
       typescript: 5.9.2
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
 
   ts-loader@9.4.4(typescript@5.9.2)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))):
     dependencies:
@@ -61652,7 +62076,7 @@ snapshots:
       range-parser: 1.2.1
       schema-utils: 4.3.2
     optionalDependencies:
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     optional: true
 
   webpack-dev-middleware@7.4.2(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))):
@@ -61991,7 +62415,7 @@ snapshots:
       webpack-dev-middleware: 7.4.2(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
       ws: 8.18.3
     optionalDependencies:
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     transitivePeerDependencies:
       - bufferutil
       - debug
@@ -62126,7 +62550,7 @@ snapshots:
   webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)):
     dependencies:
       typed-assert: 1.0.9
-      webpack: 5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)
+      webpack: 5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5)
     optionalDependencies:
       html-webpack-plugin: 5.6.3(@rspack/core@1.4.11(@swc/helpers@0.5.17))(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
 
@@ -62148,7 +62572,7 @@ snapshots:
 
   webpack-virtual-modules@0.6.2: {}
 
-  webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5):
+  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.8
@@ -62172,7 +62596,7 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 4.3.2
       tapable: 2.2.2
-      terser-webpack-plugin: 5.3.14(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
+      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))
       watchpack: 2.4.4
       webpack-sources: 3.3.3
     transitivePeerDependencies:
@@ -62180,7 +62604,7 @@ snapshots:
       - esbuild
       - uglify-js
 
-  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19):
+  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.17.19):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.8
@@ -62204,7 +62628,7 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 4.3.2
       tapable: 2.2.2
-      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.11))(esbuild@0.17.19))
+      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.17.19)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
       watchpack: 2.4.4
       webpack-sources: 3.3.3
     transitivePeerDependencies:
@@ -62212,7 +62636,7 @@ snapshots:
       - esbuild
       - uglify-js
 
-  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.17.19):
+  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.8
@@ -62236,7 +62660,7 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 4.3.2
       tapable: 2.2.2
-      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.17.19)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17)))
+      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0))
       watchpack: 2.4.4
       webpack-sources: 3.3.3
     transitivePeerDependencies:
@@ -62244,7 +62668,7 @@ snapshots:
       - esbuild
       - uglify-js
 
-  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0):
+  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)(webpack-cli@5.1.4):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.8
@@ -62268,15 +62692,17 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 4.3.2
       tapable: 2.2.2
-      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)(webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0))
+      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)(webpack@5.101.0)
       watchpack: 2.4.4
       webpack-sources: 3.3.3
+    optionalDependencies:
+      webpack-cli: 5.1.4(webpack@5.101.0)
     transitivePeerDependencies:
       - '@swc/core'
       - esbuild
       - uglify-js
 
-  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)(webpack-cli@5.1.4):
+  webpack@5.101.0(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.25.5):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.8
@@ -62300,11 +62726,9 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 4.3.2
       tapable: 2.2.2
-      terser-webpack-plugin: 5.3.14(@swc/core@1.13.3(@swc/helpers@0.5.17))(esbuild@0.23.0)(webpack@5.101.0)
+      terser-webpack-plugin: 5.3.14(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5)(webpack@5.101.0(@swc/core@1.11.31(@swc/helpers@0.5.17))(esbuild@0.25.5))
       watchpack: 2.4.4
       webpack-sources: 3.3.3
-    optionalDependencies:
-      webpack-cli: 5.1.4(webpack@5.101.0)
     transitivePeerDependencies:
       - '@swc/core'
       - esbuild