diff --git a/.vscode/launch.json b/.vscode/launch.json index 25c94a2b4243..196a21e2f0d6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,86 +1,88 @@ { - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug Rspack", - "program": "node", - "args": [ - "packages/rspack-cli/bin/rspack.js", - "${input:rspackCommand}", - "-c", - "${input:rspackConfigPath}" - ], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug Jest Test", - "program": "node", - "args": [ - "--experimental-vm-modules", - "../../node_modules/jest-cli/bin/jest", - "--runInBand", - "${input:pickTestFile}", - "-t", - "${input:pickPattern}" - ], - "cwd": "${workspaceFolder}/packages/rspack-test-tools", - "initCommands": ["settings set target.process.follow-fork-mode child"] - }, - { - "name": "Attach JavaScript", - "processId": "${command:PickProcess}", - "request": "attach", - "skipFiles": [ - "/**" - ], - "type": "node" - }, - { - "type": "lldb", - "request": "attach", - "name": "Attach Rust", - "pid": "${command:pickMyProcess}" - }, - ], - "inputs": [ - { - "id": "pickTest", - "type": "command", - "command": "extension.pickTest", - }, - { - "id": "rspackCommand", - "type": "pickString", - "options": [ - "build", - "dev" - ], - "description": "choose build or dev mode", - "default": "dev" - }, - { - "id": "rspackConfigPath", - "type": "promptString", - "description": "the rspack config path of your project", - "default": "examples/basic/rspack.config.cjs" - }, - { - "id": "pickTestFile", - "type": "command", - "command": "shellCommand.execute", - "args": { - "command":"ls -alh packages/rspack-test-tools/tests/*.test.js | awk {'print $9'}", - "description": "pick test file" - } - }, - { - "id": "pickPattern", - "type": "promptString", - "description": "pattern to filter test files", - "default": "" - } - ] + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug Rspack", + "program": "node", + "args": [ + "packages/rspack-cli/bin/rspack.js", + "${input:rspackCommand}", + "-c", + "${input:rspackConfigPath}" + ], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug Jest Test", + "program": "node", + "args": [ + "--experimental-vm-modules", + "../../node_modules/jest-cli/bin/jest", + "--runInBand", + "${input:pickTestFile}", + "-t", + "${input:pickPattern}" + ], + "cwd": "${workspaceFolder}/packages/rspack-test-tools", + "initCommands": [ + "settings set target.process.follow-fork-mode child" + ] + }, + { + "name": "Attach JavaScript", + "processId": "${command:PickProcess}", + "request": "attach", + "skipFiles": [ + "/**" + ], + "type": "node" + }, + { + "type": "lldb", + "request": "attach", + "name": "Attach Rust", + "pid": "${command:pickMyProcess}" + } + ], + "inputs": [ + { + "id": "pickTest", + "type": "command", + "command": "extension.pickTest" + }, + { + "id": "rspackCommand", + "type": "pickString", + "options": [ + "build", + "dev" + ], + "description": "choose build or dev mode", + "default": "dev" + }, + { + "id": "rspackConfigPath", + "type": "promptString", + "description": "the rspack config path of your project", + "default": "examples/basic/rspack.config.cjs" + }, + { + "id": "pickTestFile", + "type": "command", + "command": "shellCommand.execute", + "args": { + "command": "ls -alh packages/rspack-test-tools/tests/*.test.js | awk {'print $9'}", + "description": "pick test file" + } + }, + { + "id": "pickPattern", + "type": "promptString", + "description": "pattern to filter test files", + "default": "" + } + ] } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fdb6cdce8160..527069ed616f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4438,6 +4438,7 @@ dependencies = [ "rspack_regex", "rspack_util", "rustc-hash", + "serde", "serde_json", "sugar_path", "swc_core", diff --git a/RFC-module-federation-tree-shaking.md b/RFC-module-federation-tree-shaking.md new file mode 100644 index 000000000000..97e628739db6 --- /dev/null +++ b/RFC-module-federation-tree-shaking.md @@ -0,0 +1,490 @@ +# RFC: Module Federation Tree-Shaking Support with External Usage Preservation + +## Summary + +This RFC describes the implementation of tree-shaking support for Module Federation shared modules, with a mechanism to preserve exports needed by external applications through usage data coordination. + +## Motivation + +Module Federation allows multiple independent applications to share code at runtime. However, tree-shaking in this context presents unique challenges: + +1. **Cross-application dependencies**: Each application can only analyze its own usage, but shared modules may have exports needed by other applications +2. **Bundle size optimization**: Without tree-shaking, shared modules include all exports even when only a subset is used across all consumers +3. **Coordination challenge**: Applications need a way to communicate which exports they need from shared modules to prevent breaking runtime dependencies + +## Detailed Design + +The solution involves a two-part system: +1. **Usage Reporting**: Each application reports what it uses from shared modules +2. **Usage Preservation**: Each application preserves exports that other applications need + +### Core Architecture + +#### Data Flow +``` +App A Build: +1. Analyzes its own usage of shared modules +2. Loads external-usage.json (what App B & C need) +3. Merges both data sets (true always wins) +4. Writes merged data to temporary file for FlagDependencyUsagePlugin +5. FlagDependencyUsagePlugin marks exports for preservation +6. Tree-shaking respects these marks +7. Emits share-usage.json asset (what App A uses, for others to consume) + +App B Build: +1. Uses App A's share-usage.json as its external-usage.json +2. Repeats the same process +``` + +### 1. ShareUsagePlugin + +Tracks usage of shared modules and coordinates with tree-shaking: + +```rust +pub struct ShareUsagePlugin { + options: ShareUsagePluginOptions, +} + +pub struct ShareUsagePluginOptions { + pub filename: String, // Output filename (default: "share-usage.json") + pub external_usage: Option, // External apps' requirements +} +``` + +**Execution Flow:** +1. **optimize_dependencies hook** (runs BEFORE FlagDependencyUsagePlugin): + - Analyzes local usage via `analyze_consume_shared_usage()` + - Loads external usage data from `external-usage.json` + - Merges both (true always wins - preserve if ANY source needs it) + - Writes temporary file to context directory for FlagDependencyUsagePlugin + +2. **after_process_assets hook** (runs AFTER optimization): + - Generates final `share-usage.json` as a build asset + - This file reports what THIS app uses (for other apps to consume) + +**Output Format (share-usage.json):** +```json +{ + "treeShake": { + "react": { + "useState": true, // This app uses useState + "useEffect": false, // This app doesn't use useEffect + "useContext": true // This app uses useContext + } + } +} +``` + +### 2. Enhanced Module Metadata + +Extended `BuildMeta` structure to track Module Federation metadata: + +```rust +pub struct BuildMeta { + // ... existing fields ... + + // Module federation fields + pub consume_shared_key: Option, + pub shared_key: Option, + pub is_shared_descendant: Option, + pub effective_shared_key: Option, +} +``` + +This metadata enables: +- Tracking which modules are shared dependencies +- Preserving share keys through the module graph +- Identifying module relationships for tree-shaking decisions + +### 3. Export Metadata Propagation + +The `ConsumeSharedPlugin` propagates export information from fallback modules to ConsumeShared modules: + +```rust +fn copy_exports_from_fallback_to_consume_shared( + module_graph: &mut ModuleGraph, + fallback_id: &ModuleIdentifier, + consume_shared_id: &ModuleIdentifier, +) -> Result<()> +``` + +This ensures ConsumeShared modules have accurate export information for analysis. + +### 4. FlagDependencyUsagePlugin Integration + +The `FlagDependencyUsagePlugin` reads the temporary share-usage.json file written by ShareUsagePlugin: + +```rust +// During optimize_dependencies phase +let usage_path = context_path.join("share-usage.json"); +if let Ok(content) = std::fs::read_to_string(&usage_path) { + // Read the merged usage data (local + external) + // Mark exports as Used/Unused based on the merged data + for module with shared_key { + if usage_data[shared_key][export_name] == true { + mark_as_used(export_name); // Preserve this export + } else { + mark_as_unused(export_name); // Safe to tree-shake + } + } +} +``` + +**Important:** The share-usage.json file read here is a TEMPORARY file containing merged data (local + external usage), not the final asset that gets emitted. + +### 5. Provide Shared Plugin Enhancements + +Updated `ProvideSharedPlugin` to: +- Track shared module keys in module metadata +- Propagate share information through the dependency graph +- Maintain module relationships for tree-shaking analysis + +## Implementation Details + +### Key Components and Their Roles + +1. **ShareUsagePlugin** (`rspack_plugin_mf/src/sharing/share_usage_plugin.rs`) + - **Purpose**: Bridge between local usage analysis and external requirements + - **Hooks into**: + - `optimize_dependencies`: Merges local + external usage, writes temp file + - `after_process_assets`: Emits final share-usage.json asset + - **Key methods**: + - `analyze_consume_shared_usage()`: Determines local usage + - `load_external_usage()`: Reads external-usage.json + - Merge logic: True always wins (preserve if ANY source needs it) + +2. **FlagDependencyUsagePlugin** (`rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs`) + - **Purpose**: Marks exports for tree-shaking based on usage data + - **Reads**: Temporary share-usage.json from context directory + - **Process**: + - For each module with a shared_key + - Looks up usage in the merged data + - Marks exports as Used (preserve) or Unused (tree-shake) + +3. **ConsumeSharedPlugin** (`rspack_plugin_mf/src/sharing/consume_shared_plugin.rs`) + - **Enhancement**: Propagates export metadata from fallback to ConsumeShared modules + - **Purpose**: Ensures accurate export information for usage analysis + +4. **Module Metadata** (`rspack_core/src/module.rs`) + - **New fields in BuildMeta**: + - `consume_shared_key`: Share key for ConsumeShared modules + - `shared_key`: Share key for shared modules + - `effective_shared_key`: Inherited share key through dependency chain + +### Test Coverage + +Added test case: `tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/` +- Validates tree-shaking behavior with shared modules +- Ensures share-usage.json generation +- Tests export preservation based on usage data + +## Benefits + +1. **Optimized Bundle Sizes**: Shared modules can be tree-shaken while preserving exports needed by remote applications +2. **Runtime Safety**: Export usage tracking ensures runtime compatibility across module boundaries +3. **Build Coordination**: Multiple builds can share usage information to optimize collectively +4. **Developer Experience**: Automatic tracking and reporting of shared module usage +5. **Backwards Compatible**: Opt-in feature that doesn't break existing Module Federation setups +6. **Cross-System Integration**: External usage preservation allows coordination with non-Rspack systems +7. **Flexible Preservation Strategies**: Support for union, intersection, and override merge strategies +8. **Conditional Preservation**: Exports can be preserved based on remote names or environments + +## Migration Strategy + +1. **Phase 1**: Deploy ShareUsagePlugin in analysis mode + - Generate usage reports without affecting tree-shaking + - Allow teams to review and validate usage data + +2. **Phase 2**: Enable tree-shaking with usage data + - Apply usage information to influence tree-shaking + - Monitor for runtime issues + +3. **Phase 3**: Cross-application coordination + - Share usage reports between applications + - Optimize shared modules globally + +## External Usage Preservation + +### Understanding the Two-File System + +The implementation uses two distinct files with different purposes: + +1. **INPUT: external-usage.json** (in project root) + - Contains exports that OTHER applications need from shared modules + - Read during build to know what to preserve for external consumers + - Typically copied from other apps' share-usage.json outputs + +2. **OUTPUT: share-usage.json** (in dist/build artifacts) + - Reports what THIS application uses from shared modules + - Generated as a build asset for OTHER applications to use + - Other apps can use this as their external-usage.json + +### Complete Build Flow Example + +``` +App A Build: +├── INPUT: external-usage.json (what App B & C need from shared modules) +├── PROCESS: +│ 1. ShareUsagePlugin analyzes App A's usage +│ 2. Loads external-usage.json +│ 3. Merges: preserves exports needed by App A OR Apps B/C +│ 4. Writes temp file for FlagDependencyUsagePlugin +│ 5. Tree-shaking preserves all marked exports +└── OUTPUT: dist/share-usage.json (what App A needs, for B & C to use) + +App B Build: +├── INPUT: external-usage.json (copied from App A's share-usage.json) +├── PROCESS: [same as above] +└── OUTPUT: dist/share-usage.json (what App B needs) +``` + +### JSON Schema for External Usage Data + +```typescript +interface ExternalUsageConfig { + // Path to external usage data file(s) + sources?: string[] | string; + // Inline external usage data + inline?: ExternalUsageData; + // Merge strategy when multiple sources conflict + mergeStrategy?: 'union' | 'intersection' | 'override'; +} + +interface ExternalUsageData { + // Version of the schema + version: '1.0'; + // Usage data organized by share key + modules: { + [shareKey: string]: { + // List of exports that must be preserved + preservedExports: string[] | '*'; + // Optional: Source system identifier + source?: string; + // Optional: Priority for merge conflicts (higher wins) + priority?: number; + // Optional: Conditions for preservation + conditions?: { + // Preserve only for specific remotes + remotes?: string[]; + // Preserve only for specific environments + environments?: string[]; + }; + }; + }; + // Optional: Global settings + settings?: { + // Default behavior for unlisted modules + defaultPreservation?: 'all' | 'none' | 'auto'; + // Timestamp of when this data was generated + timestamp?: string; + }; +} +``` + +### External Usage Data Example + +```json +{ + "version": "1.0", + "modules": { + "react": { + "preservedExports": ["useState", "useEffect", "useContext", "memo"], + "source": "remote-app-1", + "priority": 10 + }, + "react-dom": { + "preservedExports": "*", + "source": "remote-app-1" + }, + "lodash": { + "preservedExports": ["debounce", "throttle", "get", "set"], + "source": "analytics-system", + "conditions": { + "remotes": ["analytics", "reporting"] + } + }, + "@company/ui-kit": { + "preservedExports": ["Button", "Modal", "Form", "Input"], + "source": "design-system", + "priority": 20 + } + }, + "settings": { + "defaultPreservation": "auto", + "timestamp": "2025-01-19T10:00:00Z" + } +} +``` + +### Aggregated Usage Format + +For coordinating across multiple applications, an aggregated format combines usage from multiple sources: + +```json +{ + "version": "1.0", + "aggregated": true, + "sources": [ + { + "id": "app1", + "url": "https://app1.example.com/share-usage.json", + "timestamp": "2025-01-19T10:00:00Z" + }, + { + "id": "app2", + "url": "https://app2.example.com/share-usage.json", + "timestamp": "2025-01-19T09:30:00Z" + } + ], + "modules": { + "react": { + "preservedExports": { + "useState": ["app1", "app2"], + "useEffect": ["app1", "app2"], + "useContext": ["app1"], + "useMemo": ["app2"], + "useCallback": ["app2"] + }, + "totalSources": 2 + } + } +} +``` + +## Configuration Example + +```javascript +// webpack.config.js +const { ModuleFederationPlugin } = require('@rspack/core').container; +const { ShareUsagePlugin } = require('@rspack/core').sharing; + +module.exports = { + plugins: [ + new ModuleFederationPlugin({ + name: 'app', + shared: { + react: { singleton: true }, + 'react-dom': { singleton: true } + } + }), + new ShareUsagePlugin({ + filename: 'share-usage.json', + // External usage configuration + externalUsage: { + // Load external usage from files + sources: [ + './external-usage/remote-app.json', + 'https://cdn.example.com/usage/aggregated.json' + ], + // Or provide inline configuration + inline: { + version: '1.0', + modules: { + 'react': { + preservedExports: ['useState', 'useEffect'], + source: 'remote-app' + } + } + }, + // Merge strategy when conflicts occur + mergeStrategy: 'union' // Preserve all exports from all sources + } + }) + ], + optimization: { + usedExports: true, + sideEffects: false + } +}; +``` + +### CLI Integration + +The external usage data can also be provided via CLI: + +```bash +# Single external usage file +rspack build --external-usage ./external-usage.json + +# Multiple sources +rspack build --external-usage app1.json --external-usage app2.json + +# Remote source +rspack build --external-usage https://cdn.example.com/usage.json +``` + +### Programmatic API + +```javascript +const { analyzeExternalUsage } = require('@rspack/core').sharing; + +// Analyze and merge multiple usage sources +async function buildWithExternalUsage() { + const externalUsage = await analyzeExternalUsage({ + sources: [ + './local-usage.json', + 'https://remote.example.com/usage.json' + ], + mergeStrategy: 'union' + }); + + // Use in webpack config + return { + plugins: [ + new ShareUsagePlugin({ + externalUsage: { inline: externalUsage } + }) + ] + }; +} +``` + +## Unresolved Questions + +1. **Cross-repository coordination**: How should usage data be shared between independently deployed applications? +2. **Version compatibility**: How to handle usage data when shared module versions differ? +3. **Dynamic imports**: How to track usage for dynamically imported shared modules? + +## Alternatives Considered + +1. **Runtime detection**: Detect usage at runtime instead of build time + - Pros: More accurate for dynamic usage + - Cons: Performance overhead, complexity + +2. **Manual configuration**: Require developers to manually specify preserved exports + - Pros: Simple implementation + - Cons: Error-prone, maintenance burden + +3. **No tree-shaking for shared modules**: Disable tree-shaking entirely for shared modules + - Pros: Guaranteed compatibility + - Cons: Larger bundle sizes + +## How It All Works Together + +### The Complete Picture + +1. **Each app reports its usage**: Via share-usage.json in build artifacts +2. **Each app preserves external needs**: Via external-usage.json input +3. **Coordination happens through file sharing**: Apps exchange their usage reports +4. **Tree-shaking is safe**: Only truly unused exports are removed + +### Key Insights + +- **share-usage.json is OUTPUT only**: It reports what this app uses for others +- **external-usage.json is INPUT only**: It tells us what others need preserved +- **The temporary file is internal**: ShareUsagePlugin → FlagDependencyUsagePlugin communication +- **True always wins**: If ANY source needs an export, it's preserved + +### Result + +This design enables optimal tree-shaking across Module Federation boundaries: +- Each application only includes exports it needs OR that others need +- No manual configuration of preserved exports required +- Automatic coordination through usage data files +- Safe runtime module sharing with minimal bundle size + +## References + +- [Webpack Module Federation Documentation](https://webpack.js.org/concepts/module-federation/) +- [Rspack Module Federation Implementation](https://github.com/web-infra-dev/rspack/tree/main/crates/rspack_plugin_mf) +- [Tree-shaking in Webpack](https://webpack.js.org/guides/tree-shaking/) \ No newline at end of file diff --git a/crates/node_binding/rspack.wasi-browser.js b/crates/node_binding/rspack.wasi-browser.js index 90882fd3d46e..ee65959b37bc 100644 --- a/crates/node_binding/rspack.wasi-browser.js +++ b/crates/node_binding/rspack.wasi-browser.js @@ -63,62 +63,4 @@ const { }, }) export default __napiModule.exports -export const Assets = __napiModule.exports.Assets -export const AsyncDependenciesBlock = __napiModule.exports.AsyncDependenciesBlock -export const Chunk = __napiModule.exports.Chunk -export const ChunkGraph = __napiModule.exports.ChunkGraph -export const ChunkGroup = __napiModule.exports.ChunkGroup -export const Chunks = __napiModule.exports.Chunks -export const CodeGenerationResult = __napiModule.exports.CodeGenerationResult -export const CodeGenerationResults = __napiModule.exports.CodeGenerationResults -export const ConcatenatedModule = __napiModule.exports.ConcatenatedModule -export const ContextModule = __napiModule.exports.ContextModule -export const Dependency = __napiModule.exports.Dependency -export const Diagnostics = __napiModule.exports.Diagnostics -export const EntryDataDto = __napiModule.exports.EntryDataDto -export const EntryDataDTO = __napiModule.exports.EntryDataDTO -export const EntryDependency = __napiModule.exports.EntryDependency -export const EntryOptionsDto = __napiModule.exports.EntryOptionsDto -export const EntryOptionsDTO = __napiModule.exports.EntryOptionsDTO -export const ExternalModule = __napiModule.exports.ExternalModule -export const JsCompilation = __napiModule.exports.JsCompilation -export const JsCompiler = __napiModule.exports.JsCompiler -export const JsContextModuleFactoryAfterResolveData = __napiModule.exports.JsContextModuleFactoryAfterResolveData -export const JsContextModuleFactoryBeforeResolveData = __napiModule.exports.JsContextModuleFactoryBeforeResolveData -export const JsDependencies = __napiModule.exports.JsDependencies -export const JsEntries = __napiModule.exports.JsEntries -export const JsExportsInfo = __napiModule.exports.JsExportsInfo -export const JsModuleGraph = __napiModule.exports.JsModuleGraph -export const JsResolver = __napiModule.exports.JsResolver -export const JsResolverFactory = __napiModule.exports.JsResolverFactory -export const JsStats = __napiModule.exports.JsStats -export const KnownBuildInfo = __napiModule.exports.KnownBuildInfo -export const Module = __napiModule.exports.Module -export const ModuleGraphConnection = __napiModule.exports.ModuleGraphConnection -export const NativeWatcher = __napiModule.exports.NativeWatcher -export const NativeWatchResult = __napiModule.exports.NativeWatchResult -export const NormalModule = __napiModule.exports.NormalModule -export const RawExternalItemFnCtx = __napiModule.exports.RawExternalItemFnCtx -export const ReadonlyResourceData = __napiModule.exports.ReadonlyResourceData -export const ResolverFactory = __napiModule.exports.ResolverFactory -export const Sources = __napiModule.exports.Sources -export const VirtualFileStore = __napiModule.exports.VirtualFileStore -export const JsVirtualFileStore = __napiModule.exports.JsVirtualFileStore -export const async = __napiModule.exports.async -export const BuiltinPluginName = __napiModule.exports.BuiltinPluginName -export const cleanupGlobalTrace = __napiModule.exports.cleanupGlobalTrace -export const EnforceExtension = __napiModule.exports.EnforceExtension -export const EXPECTED_RSPACK_CORE_VERSION = __napiModule.exports.EXPECTED_RSPACK_CORE_VERSION -export const formatDiagnostic = __napiModule.exports.formatDiagnostic -export const JsLoaderState = __napiModule.exports.JsLoaderState -export const JsRspackSeverity = __napiModule.exports.JsRspackSeverity -export const loadBrowserslist = __napiModule.exports.loadBrowserslist -export const minify = __napiModule.exports.minify -export const minifySync = __napiModule.exports.minifySync -export const RawRuleSetConditionType = __napiModule.exports.RawRuleSetConditionType -export const registerGlobalTrace = __napiModule.exports.registerGlobalTrace -export const RegisterJsTapKind = __napiModule.exports.RegisterJsTapKind -export const sync = __napiModule.exports.sync -export const syncTraceEvent = __napiModule.exports.syncTraceEvent -export const transform = __napiModule.exports.transform -export const transformSync = __napiModule.exports.transformSync + diff --git a/crates/node_binding/rspack.wasi.cjs b/crates/node_binding/rspack.wasi.cjs index 6a77515a8fa4..1ad96db4aac4 100644 --- a/crates/node_binding/rspack.wasi.cjs +++ b/crates/node_binding/rspack.wasi.cjs @@ -108,62 +108,4 @@ const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule }, }) module.exports = __napiModule.exports -module.exports.Assets = __napiModule.exports.Assets -module.exports.AsyncDependenciesBlock = __napiModule.exports.AsyncDependenciesBlock -module.exports.Chunk = __napiModule.exports.Chunk -module.exports.ChunkGraph = __napiModule.exports.ChunkGraph -module.exports.ChunkGroup = __napiModule.exports.ChunkGroup -module.exports.Chunks = __napiModule.exports.Chunks -module.exports.CodeGenerationResult = __napiModule.exports.CodeGenerationResult -module.exports.CodeGenerationResults = __napiModule.exports.CodeGenerationResults -module.exports.ConcatenatedModule = __napiModule.exports.ConcatenatedModule -module.exports.ContextModule = __napiModule.exports.ContextModule -module.exports.Dependency = __napiModule.exports.Dependency -module.exports.Diagnostics = __napiModule.exports.Diagnostics -module.exports.EntryDataDto = __napiModule.exports.EntryDataDto -module.exports.EntryDataDTO = __napiModule.exports.EntryDataDTO -module.exports.EntryDependency = __napiModule.exports.EntryDependency -module.exports.EntryOptionsDto = __napiModule.exports.EntryOptionsDto -module.exports.EntryOptionsDTO = __napiModule.exports.EntryOptionsDTO -module.exports.ExternalModule = __napiModule.exports.ExternalModule -module.exports.JsCompilation = __napiModule.exports.JsCompilation -module.exports.JsCompiler = __napiModule.exports.JsCompiler -module.exports.JsContextModuleFactoryAfterResolveData = __napiModule.exports.JsContextModuleFactoryAfterResolveData -module.exports.JsContextModuleFactoryBeforeResolveData = __napiModule.exports.JsContextModuleFactoryBeforeResolveData -module.exports.JsDependencies = __napiModule.exports.JsDependencies -module.exports.JsEntries = __napiModule.exports.JsEntries -module.exports.JsExportsInfo = __napiModule.exports.JsExportsInfo -module.exports.JsModuleGraph = __napiModule.exports.JsModuleGraph -module.exports.JsResolver = __napiModule.exports.JsResolver -module.exports.JsResolverFactory = __napiModule.exports.JsResolverFactory -module.exports.JsStats = __napiModule.exports.JsStats -module.exports.KnownBuildInfo = __napiModule.exports.KnownBuildInfo -module.exports.Module = __napiModule.exports.Module -module.exports.ModuleGraphConnection = __napiModule.exports.ModuleGraphConnection -module.exports.NativeWatcher = __napiModule.exports.NativeWatcher -module.exports.NativeWatchResult = __napiModule.exports.NativeWatchResult -module.exports.NormalModule = __napiModule.exports.NormalModule -module.exports.RawExternalItemFnCtx = __napiModule.exports.RawExternalItemFnCtx -module.exports.ReadonlyResourceData = __napiModule.exports.ReadonlyResourceData -module.exports.ResolverFactory = __napiModule.exports.ResolverFactory -module.exports.Sources = __napiModule.exports.Sources -module.exports.VirtualFileStore = __napiModule.exports.VirtualFileStore -module.exports.JsVirtualFileStore = __napiModule.exports.JsVirtualFileStore -module.exports.async = __napiModule.exports.async -module.exports.BuiltinPluginName = __napiModule.exports.BuiltinPluginName -module.exports.cleanupGlobalTrace = __napiModule.exports.cleanupGlobalTrace -module.exports.EnforceExtension = __napiModule.exports.EnforceExtension -module.exports.EXPECTED_RSPACK_CORE_VERSION = __napiModule.exports.EXPECTED_RSPACK_CORE_VERSION -module.exports.formatDiagnostic = __napiModule.exports.formatDiagnostic -module.exports.JsLoaderState = __napiModule.exports.JsLoaderState -module.exports.JsRspackSeverity = __napiModule.exports.JsRspackSeverity -module.exports.loadBrowserslist = __napiModule.exports.loadBrowserslist -module.exports.minify = __napiModule.exports.minify -module.exports.minifySync = __napiModule.exports.minifySync -module.exports.RawRuleSetConditionType = __napiModule.exports.RawRuleSetConditionType -module.exports.registerGlobalTrace = __napiModule.exports.registerGlobalTrace -module.exports.RegisterJsTapKind = __napiModule.exports.RegisterJsTapKind -module.exports.sync = __napiModule.exports.sync -module.exports.syncTraceEvent = __napiModule.exports.syncTraceEvent -module.exports.transform = __napiModule.exports.transform -module.exports.transformSync = __napiModule.exports.transformSync + diff --git a/crates/rspack_binding_api/src/module.rs b/crates/rspack_binding_api/src/module.rs index 02bbf043f14b..ee72c8001779 100644 --- a/crates/rspack_binding_api/src/module.rs +++ b/crates/rspack_binding_api/src/module.rs @@ -850,6 +850,10 @@ impl From for BuildMeta { default_object, side_effect_free, exports_final_name, + consume_shared_key: None, + shared_key: None, + is_shared_descendant: None, + effective_shared_key: None, } } } diff --git a/crates/rspack_core/src/module.rs b/crates/rspack_core/src/module.rs index d17a29397363..953af8dd3089 100644 --- a/crates/rspack_core/src/module.rs +++ b/crates/rspack_core/src/module.rs @@ -206,6 +206,15 @@ pub struct BuildMeta { pub side_effect_free: Option, #[serde(skip_serializing_if = "Option::is_none")] pub exports_final_name: Option>, + // Module federation fields + #[serde(skip_serializing_if = "Option::is_none")] + pub consume_shared_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub shared_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_shared_descendant: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_shared_key: Option, } // webpack build info @@ -285,6 +294,10 @@ pub trait Module: self.build_info().module_argument } + fn get_consume_shared_key(&self) -> Option<&String> { + self.build_meta().consume_shared_key.as_ref() + } + fn get_exports_type( &self, module_graph: &ModuleGraph, diff --git a/crates/rspack_plugin_javascript/Cargo.toml b/crates/rspack_plugin_javascript/Cargo.toml index d42205454ac2..fd80b0c934fa 100644 --- a/crates/rspack_plugin_javascript/Cargo.toml +++ b/crates/rspack_plugin_javascript/Cargo.toml @@ -35,6 +35,7 @@ rspack_paths = { workspace = true } rspack_regex = { workspace = true } rspack_util = { workspace = true } rustc-hash = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } sugar_path = { workspace = true } swc_core = { workspace = true, features = [ diff --git a/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs b/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs index 0ad2d21db9df..b4238667d591 100644 --- a/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs +++ b/crates/rspack_plugin_javascript/src/plugin/flag_dependency_usage_plugin.rs @@ -14,10 +14,25 @@ use rspack_error::Result; use rspack_hook::{plugin, plugin_hook}; use rspack_util::{queue::Queue, swc::join_atom}; use rustc_hash::FxHashMap as HashMap; +use serde::Deserialize; type ProcessBlockTask = (ModuleOrAsyncDependenciesBlock, Option, bool); type NonNestedTask = (Option, bool, Vec); +#[derive(Deserialize)] +struct ShareUsageReport { + #[serde(rename = "treeShake", default)] + tree_shake: std::collections::HashMap, +} + +#[derive(Deserialize)] +struct ShareUsageEntry { + #[serde(flatten)] + exports: std::collections::HashMap, + #[serde(rename = "chunkCharacteristics", default)] + _chunk_characteristics: serde_json::Value, +} + #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] enum ModuleOrAsyncDependenciesBlock { Module(ModuleIdentifier), @@ -47,7 +62,45 @@ impl<'a> FlagDependencyUsagePluginProxy<'a> { } fn apply(&mut self) { + let context_path = self.compilation.options.context.as_path().to_path_buf(); let mut module_graph = self.compilation.get_module_graph_mut(); + + // Apply shared module usage information from share-usage.json if available + let usage_path = context_path.join("share-usage.json"); + if let Ok(content) = std::fs::read_to_string(&usage_path) { + if let Ok(report) = serde_json::from_str::(&content) { + let module_ids: Vec<_> = module_graph.modules().keys().copied().collect(); + for module_id in module_ids { + let module = module_graph + .module_by_identifier(&module_id) + .expect("module not found"); + let shared_key = { + let meta = module.build_meta(); + meta + .effective_shared_key + .clone() + .or_else(|| meta.shared_key.clone()) + }; + if let Some(key) = shared_key { + if let Some(usage) = report.tree_shake.get(&key) { + let exports_info = module_graph.get_exports_info(&module_id); + let exports_info_data = exports_info.as_data_mut(&mut module_graph); + for (export_name, used) in &usage.exports { + let export_atom = rspack_util::atom::Atom::from(export_name.as_str()); + let info = exports_info_data.ensure_owned_export_info(&export_atom); + let state = if *used { + UsageState::Used + } else { + UsageState::Unused + }; + info.set_used(state, None); + } + } + } + } + } + } + for mgm in module_graph.module_graph_modules().values() { self .exports_info_module_map diff --git a/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs b/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs index ea1fc35583dc..083deedbc60a 100644 --- a/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs +++ b/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs @@ -18,6 +18,7 @@ use super::{ federation_runtime_dependency::FederationRuntimeDependency, hoist_container_references_plugin::HoistContainerReferencesPlugin, }; +use crate::{ShareUsagePlugin, ShareUsagePluginOptions}; #[derive(Debug, Default, Deserialize, Clone)] pub struct ModuleFederationRuntimePluginOptions { @@ -89,6 +90,7 @@ impl Plugin for ModuleFederationRuntimePlugin { // Apply supporting plugins EmbedFederationRuntimePlugin::default().apply(ctx)?; HoistContainerReferencesPlugin::default().apply(ctx)?; + ShareUsagePlugin::new(ShareUsagePluginOptions::default()).apply(ctx)?; Ok(()) } diff --git a/crates/rspack_plugin_mf/src/lib.rs b/crates/rspack_plugin_mf/src/lib.rs index 6c01ac47d405..199220c76450 100644 --- a/crates/rspack_plugin_mf/src/lib.rs +++ b/crates/rspack_plugin_mf/src/lib.rs @@ -21,6 +21,7 @@ pub use sharing::{ CodeGenerationDataShareInit, DataInitStage, ShareInitData, ShareRuntimeModule, }, share_runtime_plugin::ShareRuntimePlugin, + share_usage_plugin::{ShareUsagePlugin, ShareUsagePluginOptions}, }; mod utils { diff --git a/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs b/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs index f60aca0aa0b1..a04bdcf44671 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs @@ -6,10 +6,10 @@ use rspack_collections::{Identifiable, Identifier}; use rspack_core::{ AsyncDependenciesBlock, AsyncDependenciesBlockIdentifier, BoxDependency, BuildContext, BuildInfo, BuildMeta, BuildResult, CodeGenerationResult, Compilation, ConcatenationScope, Context, - DependenciesBlock, DependencyId, FactoryMeta, LibIdentOptions, Module, ModuleGraph, - ModuleIdentifier, ModuleType, RuntimeGlobals, RuntimeSpec, SourceType, async_module_factory, - impl_module_meta_info, impl_source_map_config, module_update_hash, rspack_sources::BoxSource, - sync_module_factory, + DependenciesBlock, DependencyId, DependencyType, FactoryMeta, LibIdentOptions, Module, + ModuleGraph, ModuleIdentifier, ModuleType, RuntimeGlobals, RuntimeSpec, SourceType, + async_module_factory, impl_module_meta_info, impl_source_map_config, module_update_hash, + rspack_sources::BoxSource, sync_module_factory, }; use rspack_error::{Result, impl_empty_diagnosable_trait}; use rspack_hash::{RspackHash, RspackHashDigest}; @@ -93,6 +93,18 @@ impl ConsumeSharedModule { source_map_kind: SourceMapKind::empty(), } } + + pub fn find_fallback_module_id(&self, module_graph: &ModuleGraph) -> Option { + for dep_id in &self.dependencies { + if let Some(dep) = module_graph.dependency_by_id(dep_id) + && matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback) + && let Some(fallback_id) = module_graph.module_identifier_by_dependency_id(dep_id) + { + return Some(*fallback_id); + } + } + None + } } impl Identifiable for ConsumeSharedModule { diff --git a/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs index 9074a7e125f5..42974127714b 100644 --- a/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/consume_shared_plugin.rs @@ -4,15 +4,18 @@ use std::{ sync::{Arc, LazyLock, Mutex, RwLock}, }; +use async_trait::async_trait; use camino::Utf8Path; use regex::Regex; use rspack_cacheable::cacheable; use rspack_core::{ BoxModule, ChunkUkey, Compilation, CompilationAdditionalTreeRuntimeRequirements, - CompilationParams, CompilerThisCompilation, Context, DependencyCategory, DependencyType, - ModuleExt, ModuleFactoryCreateData, NormalModuleCreateData, NormalModuleFactoryCreateModule, - NormalModuleFactoryFactorize, Plugin, ResolveOptionsWithDependencyType, ResolveResult, Resolver, - RuntimeGlobals, + CompilationFinishModules, CompilationParams, CompilerThisCompilation, Context, + DependencyCategory, DependencyType, ExportsInfoGetter, ModuleExt, ModuleFactoryCreateData, + ModuleGraph, ModuleIdentifier, ModuleType, NormalModuleCreateData, + NormalModuleFactoryCreateModule, NormalModuleFactoryFactorize, NormalModuleFactoryModule, Plugin, + PrefetchExportsInfoMode, ProvidedExports, ResolveOptionsWithDependencyType, ResolveResult, + Resolver, RuntimeGlobals, }; use rspack_error::{Diagnostic, Result, error}; use rspack_fs::ReadableFileSystem; @@ -226,7 +229,7 @@ impl ConsumeSharedPlugin { context: &Context, request: &str, config: Arc, - mut add_diagnostic: impl FnMut(Diagnostic), + add_diagnostic: &mut impl FnMut(Diagnostic), ) -> Option { let mut required_version_warning = |details: &str| { add_diagnostic(Diagnostic::warn( @@ -300,13 +303,317 @@ impl ConsumeSharedPlugin { } } + /// Extract share key from ConsumeShared module identifier + /// Format: "consume shared module ({share_scope}) {share_key}@{version}..." + fn extract_share_key_from_identifier(identifier: &str) -> Option { + // Use regex to extract share_key from the identifier + static SHARE_KEY_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"consume shared module \([^)]+\) ([^@]+)@").expect("valid regex") + }); + + SHARE_KEY_REGEX + .captures(identifier) + .and_then(|caps| caps.get(1)) + .map(|m| m.as_str().to_string()) + } + + /// Set consume_shared_key in the fallback module's BuildMeta for tree-shaking macro support + fn set_consume_shared_key_in_fallback( + compilation: &mut Compilation, + consume_shared_id: &ModuleIdentifier, + ) -> Result<()> { + // First, get the share_key from the ConsumeShared module + let share_key = { + let module_graph = compilation.get_module_graph(); + if let Some(consume_shared_module) = module_graph.module_by_identifier(consume_shared_id) { + consume_shared_module.get_consume_shared_key().cloned() + } else { + None + } + }; + + if let Some(share_key) = share_key { + // Find the fallback module identifier + let fallback_id = { + let module_graph = compilation.get_module_graph(); + if let Some(consume_shared_module) = module_graph.module_by_identifier(consume_shared_id) { + if let Some(consume_shared) = consume_shared_module + .as_any() + .downcast_ref::() + { + consume_shared.find_fallback_module_id(&module_graph) + } else { + None + } + } else { + None + } + }; + + // If we have a fallback, set the consume_shared_key in its BuildMeta + if let Some(fallback_id) = fallback_id { + let mut module_graph = compilation.get_module_graph_mut(); + if let Some(fallback_module) = module_graph.module_by_identifier_mut(&fallback_id) { + // Set the consume_shared_key in the fallback module's BuildMeta + fallback_module.build_meta_mut().consume_shared_key = Some(share_key); + } + } + } + + Ok(()) + } + + /// Copy metadata from fallback module to ConsumeShared module + fn copy_fallback_metadata_to_consume_shared( + compilation: &mut Compilation, + consume_shared_id: &ModuleIdentifier, + ) -> Result<()> { + // First, find the fallback module identifier + let fallback_id = { + let module_graph = compilation.get_module_graph(); + if let Some(consume_shared_module) = module_graph.module_by_identifier(consume_shared_id) { + if let Some(consume_shared) = consume_shared_module + .as_any() + .downcast_ref::() + { + consume_shared.find_fallback_module_id(&module_graph) + } else { + None + } + } else { + None + } + }; + + // If we have a fallback, copy the export metadata + if let Some(fallback_id) = fallback_id { + let mut module_graph = compilation.get_module_graph_mut(); + + // Copy export information from fallback to ConsumeShared + Self::copy_exports_from_fallback_to_consume_shared( + &mut module_graph, + &fallback_id, + consume_shared_id, + )?; + } + + Ok(()) + } + + /// Copy export information from fallback module to ConsumeShared module + fn copy_exports_from_fallback_to_consume_shared( + module_graph: &mut ModuleGraph, + fallback_id: &ModuleIdentifier, + consume_shared_id: &ModuleIdentifier, + ) -> Result<()> { + use rspack_core::ExportProvided; + + // Get exports info for both modules + let fallback_exports_info = module_graph.get_exports_info(fallback_id); + let consume_shared_exports_info = module_graph.get_exports_info(consume_shared_id); + + // Get the fallback module's provided exports using prefetched mode + let prefetched_fallback = ExportsInfoGetter::prefetch( + &fallback_exports_info, + module_graph, + PrefetchExportsInfoMode::Full, + ); + + let fallback_provided = prefetched_fallback.get_provided_exports(); + + // Copy the provided exports to the ConsumeShared module + match fallback_provided { + ProvidedExports::ProvidedNames(export_names) => { + // Copy each specific export from fallback to ConsumeShared + for export_name in export_names { + // Get or create export info for this export in the ConsumeShared module + let consume_shared_export_info = + consume_shared_exports_info.get_export_info(module_graph, &export_name); + let fallback_export_info = + fallback_exports_info.get_export_info(module_graph, &export_name); + + // Copy the provided status + if let Some(provided) = fallback_export_info.as_data(module_graph).provided() { + consume_shared_export_info + .as_data_mut(module_graph) + .set_provided(Some(provided)); + } else { + // Default to provided if not explicitly set in fallback + consume_shared_export_info + .as_data_mut(module_graph) + .set_provided(Some(ExportProvided::Provided)); + } + + // Copy can_mangle_provide status + if let Some(can_mangle) = fallback_export_info + .as_data(module_graph) + .can_mangle_provide() + { + consume_shared_export_info + .as_data_mut(module_graph) + .set_can_mangle_provide(Some(can_mangle)); + } + + // Copy exports_info if it exists (for nested exports) + if let Some(nested_exports_info) = + fallback_export_info.as_data(module_graph).exports_info() + { + consume_shared_export_info + .as_data_mut(module_graph) + .set_exports_info(Some(nested_exports_info)); + } + + // Note: Usage state copying is handled by FlagDependencyUsagePlugin + // We only copy provision metadata here + + // Copy terminal binding information if available + let terminal_binding = fallback_export_info + .as_data(module_graph) + .terminal_binding(); + if terminal_binding { + consume_shared_export_info + .as_data_mut(module_graph) + .set_terminal_binding(terminal_binding); + } + } + + // Mark the ConsumeShared module as having complete provide info + consume_shared_exports_info.set_has_provide_info(module_graph); + + // Set the "other exports" to not provided (since we copied all specific exports) + consume_shared_exports_info.set_unknown_exports_provided( + module_graph, + false, // not provided + None, // no exclude exports + None, // no can_mangle + None, // no terminal_binding + None, // no target_key + ); + } + ProvidedExports::ProvidedAll => { + // If fallback provides all exports, mark ConsumeShared the same way + consume_shared_exports_info.set_unknown_exports_provided( + module_graph, + true, // provided + None, // no exclude exports + None, // no can_mangle + None, // no terminal_binding + None, // no target_key + ); + consume_shared_exports_info.set_has_provide_info(module_graph); + } + ProvidedExports::Unknown => { + // Keep unknown status - don't copy anything + } + } + + Ok(()) + } + + /// Enhanced metadata copying that also analyzes usage through incoming connections + fn enhanced_copy_fallback_metadata_to_consume_shared( + compilation: &mut Compilation, + consume_shared_id: &ModuleIdentifier, + ) -> Result<()> { + // Note: Enhanced analysis disabled due to borrow checker issues + // ShareUsagePlugin provides this functionality instead + + // First, do the standard export metadata copying + Self::copy_fallback_metadata_to_consume_shared(compilation, consume_shared_id)?; + + /* Enhanced analysis commented out due to borrow checker issues + // Then, enhance with usage analysis from incoming connections + let mut module_graph = compilation.get_module_graph_mut(); + + // Analyze incoming connections to track actual usage + let incoming_connections: Vec<_> = module_graph + .get_incoming_connections(consume_shared_id) + .collect(); + + for connection in incoming_connections { + if let Some(dependency) = module_graph.dependency_by_id(&connection.dependency_id) { + // Use get_referenced_exports to extract specific export names + let referenced_exports = dependency.get_referenced_exports( + &module_graph, + &ModuleGraphCacheArtifact::default(), + None, + ); + + // Process referenced exports and mark them as used in the ConsumeShared module + for export_ref in referenced_exports { + match export_ref { + ExtendedReferencedExport::Array(names) => { + for name in names { + let export_atom = rspack_util::atom::Atom::from(name.as_str()); + let exports_info = module_graph.get_exports_info(consume_shared_id); + let export_info = exports_info.get_export_info(&mut module_graph, &export_atom); + + // Usage state is handled by FlagDependencyUsagePlugin + // Just mark as provided + + export_info.as_data_mut(&mut module_graph).set_provided( + Some(rspack_core::ExportProvided::Provided), + ); + } + }, + ExtendedReferencedExport::Export(export_info) => { + if !export_info.name.is_empty() { + for name in export_info.name { + let export_atom = rspack_util::atom::Atom::from(name.as_str()); + let exports_info = module_graph.get_exports_info(consume_shared_id); + let export_info = exports_info.get_export_info(&mut module_graph, &export_atom); + + // Usage state is handled by FlagDependencyUsagePlugin + // Just mark as provided + + export_info.as_data_mut(&mut module_graph).set_provided( + Some(rspack_core::ExportProvided::Provided), + ); + } + } + }, + ExtendedReferencedExport::Export(_) => { + // This might be a namespace import or similar - analyze further if needed + let exports_info = module_graph.get_exports_info(consume_shared_id); + + // For namespace imports, we may need to mark all exports as potentially used + // This is a conservative approach to ensure tree-shaking doesn't remove needed exports + let prefetched = ExportsInfoGetter::prefetch( + &exports_info, + &module_graph, + PrefetchExportsInfoMode::Full, + ); + + if let ProvidedExports::ProvidedNames(export_names) = prefetched.get_provided_exports() { + for export_name in export_names { + let export_info = exports_info.get_export_info(&mut module_graph, &export_name); + // Usage state is handled by FlagDependencyUsagePlugin + // Just mark as provided + export_info.as_data_mut(&mut module_graph).set_provided( + Some(rspack_core::ExportProvided::Provided), + ); + } + } + }, + _ => { + // Handle other cases if needed - potentially log for debugging + } + } + } + } + } + */ + + Ok(()) + } + async fn create_consume_shared_module( &self, context: &Context, request: &str, config: Arc, - mut add_diagnostic: impl FnMut(Diagnostic), - ) -> ConsumeSharedModule { + add_diagnostic: &mut impl FnMut(Diagnostic), + ) -> Result { let direct_fallback = matches!(&config.import, Some(i) if RELATIVE_REQUEST.is_match(i) | ABSOLUTE_REQUEST.is_match(i)); let import_resolved = match &config.import { None => None, @@ -339,7 +646,8 @@ impl ConsumeSharedPlugin { let required_version = self .get_required_version(context, request, config.clone(), add_diagnostic) .await; - ConsumeSharedModule::new( + + Ok(ConsumeSharedModule::new( if direct_fallback { self.get_context() } else { @@ -359,7 +667,7 @@ impl ConsumeSharedPlugin { singleton: config.singleton, eager: config.eager, }, - ) + )) } } @@ -381,6 +689,43 @@ async fn this_compilation( Ok(()) } +#[plugin_hook(CompilationFinishModules for ConsumeSharedPlugin)] +async fn finish_modules(&self, compilation: &mut Compilation) -> Result<()> { + // Find all ConsumeShared modules and copy metadata from their fallbacks + let consume_shared_modules: Vec = compilation + .get_module_graph() + .modules() + .keys() + .filter(|id| { + if let Some(module) = compilation.get_module_graph().module_by_identifier(id) { + module.module_type() == &ModuleType::ConsumeShared + } else { + false + } + }) + .copied() + .collect(); + + // Process each ConsumeShared module individually to avoid borrow checker issues + for consume_shared_id in consume_shared_modules { + // First, set the consume_shared_key in the fallback module's BuildMeta + Self::set_consume_shared_key_in_fallback(compilation, &consume_shared_id)?; + + if self.options.enhanced { + // Use enhanced copying that includes usage analysis + Self::enhanced_copy_fallback_metadata_to_consume_shared(compilation, &consume_shared_id)?; + } else { + // Use standard metadata copying + Self::copy_fallback_metadata_to_consume_shared(compilation, &consume_shared_id)?; + } + } + + // Phase 2: Unified shared detection optimization for all modules + Self::mark_shared_descendants(compilation)?; + + Ok(()) +} + #[plugin_hook(NormalModuleFactoryFactorize for ConsumeSharedPlugin)] async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result> { let dep = data.dependencies[0] @@ -395,17 +740,20 @@ async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result return Ok(Some(module.boxed())), + Err(_) => return Ok(None), // Error already handled via diagnostic + } } for (prefix, options) in &consumes.prefixed { if request.starts_with(prefix) { let remainder = &request[prefix.len()..]; - let module = self + let mut add_diagnostic = |d| data.diagnostics.push(d); + match self .create_consume_shared_module( &data.context, request, @@ -420,10 +768,13 @@ async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result return Ok(Some(module.boxed())), + Err(_) => return Ok(None), // Error already handled via diagnostic + } } } Ok(None) @@ -444,16 +795,60 @@ async fn create_module( let resource = &create_data.resource_resolve_data.resource; let consumes = self.get_matched_consumes(); if let Some(options) = consumes.resolved.get(resource) { - let module = self - .create_consume_shared_module(&data.context, resource, options.clone(), |d| { - data.diagnostics.push(d) - }) - .await; - return Ok(Some(module.boxed())); + let mut add_diagnostic = |d| data.diagnostics.push(d); + match self + .create_consume_shared_module( + &data.context, + resource, + options.clone(), + &mut add_diagnostic, + ) + .await + { + Ok(module) => return Ok(Some(module.boxed())), + Err(_) => return Ok(None), // Error already handled via diagnostic + } } Ok(None) } +#[plugin_hook(NormalModuleFactoryModule for ConsumeSharedPlugin)] +async fn normal_module_factory_module( + &self, + data: &mut ModuleFactoryCreateData, + _create_data: &mut NormalModuleCreateData, + module: &mut BoxModule, +) -> Result<()> { + // Check if this is a ConsumeSharedFallback dependency + if !data.dependencies.is_empty() + && matches!( + data.dependencies[0].dependency_type(), + DependencyType::ConsumeSharedFallback + ) + { + // Get the issuer identifier (ConsumeShared module) + if let Some(issuer_id) = &data.issuer_identifier { + // Try to get the share_key from the issuer module + // Since we're in the module factory, we need to check if the issuer is a ConsumeSharedModule + // The issuer should be in the dependency's context + if let Some(_dep) = data.dependencies[0] + .as_any() + .downcast_ref::( + ) { + // Extract share key from the issuer identifier + // ConsumeShared module identifiers have the format: + // "consume shared module ({share_scope}) {share_key}@{version}..." + let issuer_str = issuer_id.to_string(); + if let Some(share_key) = Self::extract_share_key_from_identifier(&issuer_str) { + // Set the consume_shared_key in the fallback module's BuildMeta + module.build_meta_mut().consume_shared_key = Some(share_key); + } + } + } + } + Ok(()) +} + #[plugin_hook(CompilationAdditionalTreeRuntimeRequirements for ConsumeSharedPlugin)] async fn additional_tree_runtime_requirements( &self, @@ -474,6 +869,139 @@ async fn additional_tree_runtime_requirements( Ok(()) } +impl ConsumeSharedPlugin { + /// Phase 2: Unified shared descendant detection optimization for ESM and CommonJS + /// Processes both ESM and CommonJS modules to maximize performance benefits + fn mark_shared_descendants(compilation: &mut Compilation) -> Result<()> { + use std::collections::{HashMap, HashSet, VecDeque}; + + // Collect all module data we need upfront to avoid borrow checker issues + let module_data: HashMap, Option, ModuleType)> = { + let module_graph = compilation.get_module_graph(); + module_graph + .modules() + .iter() + .map(|(id, module)| { + let build_meta = module.build_meta(); + ( + *id, + ( + build_meta.esm, + build_meta.consume_shared_key.clone(), + build_meta.shared_key.clone(), + *module.module_type(), + ), + ) + }) + .collect() + }; + + // Collect all connections upfront + let connections: HashMap> = { + let module_graph = compilation.get_module_graph(); + module_data + .keys() + .map(|module_id| { + let outgoing: Vec<_> = module_graph + .get_outgoing_connections(module_id) + .map(|conn| *conn.module_identifier()) + .collect(); + (*module_id, outgoing) + }) + .collect() + }; + + let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + let mut shared_descendants = HashSet::new(); + let mut effective_keys = HashMap::new(); + + // Step 1: Find directly shared modules (both ESM and CommonJS) + for (module_id, (_is_esm, consume_shared_key, shared_key, module_type)) in &module_data { + // Phase 2: Process all modules, not just ESM + + // Check if this is a directly shared module + let is_directly_shared = consume_shared_key.is_some() + || shared_key.is_some() + || module_type == &ModuleType::ConsumeShared + || module_type == &ModuleType::ProvideShared; + + if is_directly_shared { + shared_descendants.insert(*module_id); + + // Set effective shared key (prioritize consume_shared_key > shared_key) + if let Some(effective_key) = consume_shared_key.clone().or_else(|| shared_key.clone()) { + effective_keys.insert(*module_id, effective_key); + } + + queue.push_back(*module_id); + } + } + + // Step 2: BFS to mark all descendants (ESM and CommonJS) + while let Some(current_id) = queue.pop_front() { + if !visited.insert(current_id) { + continue; + } + + let parent_shared_key = effective_keys.get(¤t_id).cloned(); + + if let Some(outgoing) = connections.get(¤t_id) { + for target_id in outgoing { + // Phase 2: Process all modules regardless of type + if let Some((_is_esm, _, _, _)) = module_data.get(target_id) { + // Check if target already processed + if shared_descendants.contains(target_id) { + continue; + } + + // Mark target as shared descendant + shared_descendants.insert(*target_id); + + // Inherit parent's shared key if target doesn't have one + if !effective_keys.contains_key(target_id) + && let Some(key) = parent_shared_key.clone() + { + effective_keys.insert(*target_id, key); + } + + queue.push_back(*target_id); + } + } + } + } + + // Step 3: Apply all mutations to the compilation + for (module_id, (_is_esm, _, _, _)) in &module_data { + // Phase 2: Apply to all modules, not just ESM + + if let Some(module) = compilation + .get_module_graph_mut() + .module_by_identifier_mut(module_id) + { + let build_meta = module.build_meta_mut(); + + // Set shared descendant status + build_meta.is_shared_descendant = Some(shared_descendants.contains(module_id)); + + // Set effective shared key if we have one + if let Some(effective_key) = effective_keys.get(module_id) { + build_meta.effective_shared_key = Some(effective_key.clone()); + + // IMPORTANT: Also set consume_shared_key if not already set + // This is needed for macro generation in ConsumeSharedExportsDependency + if build_meta.consume_shared_key.is_none() { + build_meta.consume_shared_key = Some(effective_key.clone()); + } + } + } + } + + Ok(()) + } +} + +#[async_trait] impl Plugin for ConsumeSharedPlugin { fn name(&self) -> &'static str { "rspack.ConsumeSharedPlugin" @@ -492,10 +1020,18 @@ impl Plugin for ConsumeSharedPlugin { .normal_module_factory_hooks .create_module .tap(create_module::new(self)); + ctx + .normal_module_factory_hooks + .module + .tap(normal_module_factory_module::new(self)); ctx .compilation_hooks .additional_tree_runtime_requirements .tap(additional_tree_runtime_requirements::new(self)); + ctx + .compilation_hooks + .finish_modules + .tap(finish_modules::new(self)); Ok(()) } } diff --git a/crates/rspack_plugin_mf/src/sharing/mod.rs b/crates/rspack_plugin_mf/src/sharing/mod.rs index a2f9e246e08b..3f1285474565 100644 --- a/crates/rspack_plugin_mf/src/sharing/mod.rs +++ b/crates/rspack_plugin_mf/src/sharing/mod.rs @@ -9,3 +9,4 @@ pub mod provide_shared_module_factory; pub mod provide_shared_plugin; pub mod share_runtime_module; pub mod share_runtime_plugin; +pub mod share_usage_plugin; diff --git a/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs b/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs index 111b02984e6a..232db6275095 100644 --- a/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/provide_shared_plugin.rs @@ -230,7 +230,7 @@ async fn normal_module_factory_module( &self, data: &mut ModuleFactoryCreateData, create_data: &mut NormalModuleCreateData, - _module: &mut BoxModule, + module: &mut BoxModule, ) -> Result<()> { let resource = &create_data.resource_resolve_data.resource; let resource_data = &create_data.resource_resolve_data; @@ -243,45 +243,106 @@ async fn normal_module_factory_module( return Ok(()); } let request = &create_data.raw_request; - { + + // Debug: Print request information + // eprintln!("DEBUG ProvideSharedPlugin: request = {}", request); + // eprintln!("DEBUG ProvideSharedPlugin: resource = {}", resource); + + // First check match_provides (for package names like 'react', 'lodash-es') + let match_config = { let match_provides = self.match_provides.read().await; - if let Some(config) = match_provides.get(request) { - self - .provide_shared_module( - request, - &config.share_key, - &config.share_scope, - config.version.as_ref(), - config.eager, - config.singleton, - config.required_version.clone(), - config.strict_version, - resource, - resource_data, - |d| data.diagnostics.push(d), - ) - .await; - } + // eprintln!("DEBUG ProvideSharedPlugin: match_provides keys = {:?}", match_provides.keys().collect::>()); + match_provides.get(request).cloned() + }; // Read lock is dropped here + + if let Some(config) = match_config { + // Set the shared_key in the module's BuildMeta for tree-shaking + // eprintln!("DEBUG ProvideSharedPlugin: Setting shared_key = {} for request = {}", config.share_key, request); + module.build_meta_mut().shared_key = Some(config.share_key.clone()); + + self + .provide_shared_module( + request, + &config.share_key, + &config.share_scope, + config.version.as_ref(), + config.eager, + config.singleton, + config.required_version.clone(), + config.strict_version, + resource, + resource_data, + |d| data.diagnostics.push(d), + ) + .await; } - for (prefix, config) in self.prefix_match_provides.read().await.iter() { - if request.starts_with(prefix) { - let remainder = &request[prefix.len()..]; - self - .provide_shared_module( - request, - &(config.share_key.to_string() + remainder), - &config.share_scope, - config.version.as_ref(), - config.eager, - config.singleton, - config.required_version.clone(), - config.strict_version, - resource, - resource_data, - |d| data.diagnostics.push(d), - ) - .await; - } + + // Second check resolved_provide_map (for relative paths like './cjs-modules/data-processor.js') + let resolved_config = { + let resolved_provide_map = self.resolved_provide_map.read().await; + // eprintln!("DEBUG ProvideSharedPlugin: resolved_provide_map keys = {:?}", resolved_provide_map.keys().collect::>()); + resolved_provide_map.get(request).cloned() + }; // Read lock is dropped here + + if let Some(config) = resolved_config { + // Set the shared_key in the module's BuildMeta for tree-shaking + // eprintln!("DEBUG ProvideSharedPlugin: Setting shared_key = {} for resolved request = {}", config.share_key, request); + module.build_meta_mut().shared_key = Some(config.share_key.clone()); + + self + .provide_shared_module( + request, + &config.share_key, + &config.share_scope, + Some(&config.version), + config.eager, + config.singleton, + config.required_version.clone(), + config.strict_version, + resource, + resource_data, + |d| data.diagnostics.push(d), + ) + .await; + } + + // Third check prefix_match_provides (for prefix patterns) + let prefix_configs: Vec<(String, ProvideOptions)> = { + let prefix_match_provides = self.prefix_match_provides.read().await; + prefix_match_provides + .iter() + .filter_map(|(prefix, config)| { + if request.starts_with(prefix) { + Some((prefix.clone(), config.clone())) + } else { + None + } + }) + .collect() + }; // Read lock is dropped here + + for (prefix, config) in prefix_configs { + let remainder = &request[prefix.len()..]; + let share_key = config.share_key.to_string() + remainder; + + // Set the shared_key in the module's BuildMeta for tree-shaking + module.build_meta_mut().shared_key = Some(share_key.clone()); + + self + .provide_shared_module( + request, + &share_key, + &config.share_scope, + config.version.as_ref(), + config.eager, + config.singleton, + config.required_version.clone(), + config.strict_version, + resource, + resource_data, + |d| data.diagnostics.push(d), + ) + .await; } Ok(()) } diff --git a/crates/rspack_plugin_mf/src/sharing/share_runtime_plugin.rs b/crates/rspack_plugin_mf/src/sharing/share_runtime_plugin.rs index 6f0b934c4b76..c65d142e7b23 100644 --- a/crates/rspack_plugin_mf/src/sharing/share_runtime_plugin.rs +++ b/crates/rspack_plugin_mf/src/sharing/share_runtime_plugin.rs @@ -11,11 +11,16 @@ use crate::ShareRuntimeModule; #[derive(Debug)] pub struct ShareRuntimePlugin { enhanced: bool, + _enable_export_usage_tracking: bool, } impl ShareRuntimePlugin { pub fn new(enhanced: bool) -> Self { - Self::new_inner(enhanced) + Self::new_inner(enhanced, false) + } + + pub fn with_export_usage_tracking(enhanced: bool, enable_export_usage_tracking: bool) -> Self { + Self::new_inner(enhanced, enable_export_usage_tracking) } } diff --git a/crates/rspack_plugin_mf/src/sharing/share_usage_plugin.rs b/crates/rspack_plugin_mf/src/sharing/share_usage_plugin.rs new file mode 100644 index 000000000000..986662d988d4 --- /dev/null +++ b/crates/rspack_plugin_mf/src/sharing/share_usage_plugin.rs @@ -0,0 +1,580 @@ +use std::{ + collections::{HashMap, HashSet}, + path::PathBuf, +}; + +use async_trait::async_trait; +use rspack_core::{ + ApplyContext, AssetInfo, Compilation, CompilationAfterProcessAssets, CompilationAsset, + CompilationOptimizeDependencies, DependenciesBlock, DependencyType, ExtendedReferencedExport, + ModuleGraph, ModuleGraphCacheArtifact, ModuleIdentifier, ModuleType, Plugin, + rspack_sources::{RawSource, SourceExt}, +}; +use rspack_error::{Error, Result}; +use rspack_hook::{plugin, plugin_hook}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShareUsageReport { + #[serde(rename = "treeShake")] + pub tree_shake: HashMap>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MergeStrategy { + Union, + Intersection, + Override, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExternalUsageModuleData { + #[serde(rename = "preservedExports")] + pub preserved_exports: PreservedExports, + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub priority: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub conditions: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PreservedExports { + All(String), // "*" + List(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PreservationConditions { + #[serde(skip_serializing_if = "Option::is_none")] + pub remotes: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub environments: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExternalUsageSettings { + #[serde( + rename = "defaultPreservation", + skip_serializing_if = "Option::is_none" + )] + pub default_preservation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExternalUsageData { + pub version: String, + pub modules: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub settings: Option, +} + +#[derive(Debug, Clone)] +pub struct ExternalUsageConfig { + pub sources: Vec, + pub inline: Option, +} + +#[derive(Debug)] +pub struct ShareUsagePluginOptions { + pub filename: String, + pub external_usage: Option, +} + +impl Default for ShareUsagePluginOptions { + fn default() -> Self { + Self { + filename: "share-usage.json".to_string(), + external_usage: None, + } + } +} + +#[plugin] +#[derive(Debug)] +pub struct ShareUsagePlugin { + options: ShareUsagePluginOptions, +} + +impl ShareUsagePlugin { + pub fn new(options: ShareUsagePluginOptions) -> Self { + Self::new_inner(options) + } + + fn load_external_usage( + &self, + _compilation: &Compilation, + ) -> Result>> { + let mut merged_usage = HashMap::new(); + + // Load external usage from configuration options + if let Some(external_config) = &self.options.external_usage { + // Load from inline data first + if let Some(inline_data) = &external_config.inline { + merged_usage.extend(inline_data.tree_shake.clone()); + } + + // Load from external files specified in config + for source in &external_config.sources { + if source.exists() { + let content = std::fs::read_to_string(source) + .map_err(|e| Error::msg(format!("Failed to read external usage file: {e}")))?; + + if let Ok(external_report) = serde_json::from_str::(&content) { + // Merge the tree_shake data from the external share-usage.json + for (share_key, external_exports) in external_report.tree_shake { + match merged_usage.entry(share_key) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + for (export_name, should_preserve) in external_exports { + // True always wins - preserve if ANY source needs it + if should_preserve { + existing.insert(export_name, true); + } + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(external_exports); + } + } + } + } + } + } + } + + Ok(merged_usage) + } + + fn merge_external_usage( + &self, + target: &mut HashMap, + source: HashMap, + strategy: &MergeStrategy, + ) { + // Merges external usage data from multiple sources + // Note: When converted to final usage map, true values always win over false + for (key, source_data) in source { + match target.entry(key) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + match strategy { + MergeStrategy::Union => { + // Merge preserved exports + let merged_exports = self.merge_preserved_exports( + &existing.preserved_exports, + &source_data.preserved_exports, + true, // union + ); + existing.preserved_exports = merged_exports; + + // Use higher priority + if let Some(source_priority) = source_data.priority { + if existing.priority.map_or(true, |p| source_priority > p) { + existing.priority = Some(source_priority); + existing.source = source_data.source; + } + } + } + MergeStrategy::Intersection => { + // Keep only common exports + let merged_exports = self.merge_preserved_exports( + &existing.preserved_exports, + &source_data.preserved_exports, + false, // intersection + ); + existing.preserved_exports = merged_exports; + } + MergeStrategy::Override => { + // Replace with source data if priority is higher + if source_data.priority.unwrap_or(0) >= existing.priority.unwrap_or(0) { + *existing = source_data; + } + } + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(source_data); + } + } + } + } + + fn merge_preserved_exports( + &self, + a: &PreservedExports, + b: &PreservedExports, + union: bool, + ) -> PreservedExports { + match (a, b) { + (PreservedExports::All(_), _) | (_, PreservedExports::All(_)) => { + PreservedExports::All("*".to_string()) + } + (PreservedExports::List(list_a), PreservedExports::List(list_b)) => { + let set_a: HashSet<_> = list_a.iter().cloned().collect(); + let set_b: HashSet<_> = list_b.iter().cloned().collect(); + + let result = if union { + set_a.union(&set_b).cloned().collect() + } else { + set_a.intersection(&set_b).cloned().collect() + }; + + PreservedExports::List(result) + } + } + } + + fn analyze_consume_shared_usage( + &self, + compilation: &Compilation, + ) -> HashMap> { + let mut usage_map = HashMap::new(); + let module_graph = compilation.get_module_graph(); + + // First, try to find ConsumeShared modules (for consumer apps) + for module_id in module_graph.modules().keys() { + if let Some(module) = module_graph.module_by_identifier(module_id) + && module.module_type() == &ModuleType::ConsumeShared + && let Some(share_key) = module.get_consume_shared_key() + { + let exports_usage = + if let Some(fallback_id) = self.find_fallback_module_id(&module_graph, module_id) { + let (used_exports, provided_exports) = + self.analyze_module_usage(&module_graph, &fallback_id, module_id); + + // Build usage map from exports + let mut usage = HashMap::new(); + for export in provided_exports { + usage.insert(export.clone(), used_exports.contains(&export)); + } + usage + } else { + HashMap::new() + }; + + usage_map.insert(share_key.to_string(), exports_usage); + } + } + + // If no ConsumeShared modules found, look for shared modules being exposed + if usage_map.is_empty() { + usage_map = self.analyze_shared_module_usage(compilation); + } + + usage_map + } + + fn analyze_shared_module_usage( + &self, + compilation: &Compilation, + ) -> HashMap> { + let mut usage_map = HashMap::new(); + let module_graph = compilation.get_module_graph(); + + // Look through all modules to find ones that are being shared + for module_id in module_graph.modules().keys() { + if let Some(module) = module_graph.module_by_identifier(module_id) { + // Check if this module is being shared by looking at its usage + let module_identifier = module.identifier().to_string(); + + // For now, we'll assume the share key is "module" based on the config + // In a real implementation, we would need to map from the shared config + if module_identifier.contains("module.js") { + let usage = self.analyze_exports_usage(&module_graph, module_id); + usage_map.insert("module".to_string(), usage); + break; + } + } + } + + usage_map + } + + fn analyze_exports_usage( + &self, + module_graph: &ModuleGraph, + module_id: &ModuleIdentifier, + ) -> HashMap { + use rspack_core::{ExportsInfoGetter, PrefetchExportsInfoMode, ProvidedExports, UsageState}; + + let mut usage_map = HashMap::new(); + + let exports_info = module_graph.get_exports_info(module_id); + let prefetched = ExportsInfoGetter::prefetch( + &exports_info, + module_graph, + PrefetchExportsInfoMode::Default, + ); + + match prefetched.get_provided_exports() { + ProvidedExports::ProvidedNames(names) => { + for export_name in names { + let export_atom = rspack_util::atom::Atom::from(export_name.as_str()); + let export_info_data = prefetched.get_read_only_export_info(&export_atom); + let usage_state = export_info_data.get_used(None); + + // Mark as used if the usage state indicates it's being used + let is_used = matches!( + usage_state, + UsageState::Used | UsageState::OnlyPropertiesUsed + ); + usage_map.insert(export_name.to_string(), is_used); + } + } + ProvidedExports::ProvidedAll => { + // If all exports are provided but we don't know specifics, + // we can't determine individual usage + usage_map.insert("*".to_string(), false); + } + ProvidedExports::Unknown => { + // Cannot determine exports statically + } + } + + usage_map + } + + fn analyze_module_usage( + &self, + module_graph: &ModuleGraph, + fallback_id: &ModuleIdentifier, + consume_shared_id: &ModuleIdentifier, + ) -> (Vec, Vec) { + use rspack_core::{ExportsInfoGetter, PrefetchExportsInfoMode, ProvidedExports, UsageState}; + + let mut used_exports = Vec::new(); + let mut provided_exports = Vec::new(); + let mut all_imported_exports = HashSet::new(); + + let exports_info = module_graph.get_exports_info(fallback_id); + let prefetched = ExportsInfoGetter::prefetch( + &exports_info, + module_graph, + PrefetchExportsInfoMode::Default, + ); + + match prefetched.get_provided_exports() { + ProvidedExports::ProvidedNames(names) => { + provided_exports = names.iter().map(|n| n.to_string()).collect(); + + for export_name in names { + let export_atom = rspack_util::atom::Atom::from(export_name.as_str()); + let export_info_data = prefetched.get_read_only_export_info(&export_atom); + let usage = export_info_data.get_used(None); + + if matches!(usage, UsageState::Used | UsageState::OnlyPropertiesUsed) + && export_name != "*" + { + used_exports.push(export_name.to_string()); + } + } + } + ProvidedExports::ProvidedAll => provided_exports = vec!["*".to_string()], + ProvidedExports::Unknown => {} + } + + for connection in module_graph.get_incoming_connections(consume_shared_id) { + if let Some(dependency) = module_graph.dependency_by_id(&connection.dependency_id) { + let referenced_exports = dependency.get_referenced_exports( + module_graph, + &ModuleGraphCacheArtifact::default(), + None, + ); + + for export_ref in referenced_exports { + let names = match export_ref { + ExtendedReferencedExport::Array(names) => { + names.into_iter().map(|n| n.to_string()).collect::>() + } + ExtendedReferencedExport::Export(export_info) => export_info + .name + .into_iter() + .map(|n| n.to_string()) + .collect::>(), + }; + + for name in names { + if !used_exports.contains(&name) { + used_exports.push(name); + } + } + } + + self.extract_dependency_imports( + dependency.as_ref(), + module_graph, + &mut all_imported_exports, + ); + } + } + + for imported in &all_imported_exports { + if provided_exports.contains(imported) && !used_exports.contains(imported) { + used_exports.push(imported.clone()); + } + } + + (used_exports, provided_exports) + } + + fn extract_dependency_imports( + &self, + dependency: &dyn rspack_core::Dependency, + module_graph: &ModuleGraph, + imports: &mut HashSet, + ) { + let referenced_exports = + dependency.get_referenced_exports(module_graph, &ModuleGraphCacheArtifact::default(), None); + + let mut found_exports = false; + for export_ref in referenced_exports { + found_exports = true; + let names = match export_ref { + ExtendedReferencedExport::Array(names) => names, + ExtendedReferencedExport::Export(export_info) => export_info.name, + }; + + for name in names { + let name_str = name.to_string(); + if !name_str.is_empty() { + imports.insert(name_str); + } + } + } + + if !found_exports { + imports.insert("default".to_string()); + } + } + + fn find_fallback_module_id( + &self, + module_graph: &ModuleGraph, + consume_shared_id: &ModuleIdentifier, + ) -> Option { + if let Some(module) = module_graph.module_by_identifier(consume_shared_id) { + for dep_id in module.get_dependencies() { + if let Some(dep) = module_graph.dependency_by_id(dep_id) + && matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback) + && let Some(fallback_id) = module_graph.module_identifier_by_dependency_id(dep_id) + { + return Some(*fallback_id); + } + } + + for block_id in module.get_blocks() { + if let Some(block) = module_graph.block_by_id(block_id) { + for dep_id in block.get_dependencies() { + if let Some(dep) = module_graph.dependency_by_id(dep_id) + && matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback) + && let Some(fallback_id) = module_graph.module_identifier_by_dependency_id(dep_id) + { + return Some(*fallback_id); + } + } + } + } + } + + None + } +} + +#[plugin_hook(CompilationOptimizeDependencies for ShareUsagePlugin)] +async fn optimize_dependencies(&self, compilation: &mut Compilation) -> Result> { + // Step 1: Analyze what THIS application uses from shared modules + let mut usage_data = self.analyze_consume_shared_usage(compilation); + + // Step 2: Load external usage data - exports that OTHER apps need (don't tree-shake these!) + let external_usage = self.load_external_usage(compilation)?; + + // Step 3: Merge both sources - the output will contain: + // - Exports this app uses (marked as true) + // - Exports other apps need (also marked as true, even if unused locally) + // - Everything else (marked as false, safe to tree-shake) + for (share_key, external_exports) in external_usage { + // Merge with existing usage data - true always wins + match usage_data.entry(share_key) { + std::collections::hash_map::Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + for (export_name, should_preserve) in external_exports { + // Only set to true, never overwrite true with false + if should_preserve { + existing.insert(export_name, true); + } + } + } + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(external_exports); + } + } + } + + // Write to context directory so FlagDependencyUsagePlugin can read it + let context_path = compilation.options.context.as_path(); + let usage_file_path = context_path.join("share-usage.json"); + + let report = ShareUsageReport { + tree_shake: usage_data, + }; + + let content = serde_json::to_string_pretty(&report) + .map_err(|e| Error::msg(format!("Failed to serialize share usage report: {e}")))?; + + // Write to filesystem for FlagDependencyUsagePlugin to read + std::fs::write(&usage_file_path, content) + .map_err(|e| Error::msg(format!("Failed to write share usage file: {e}")))?; + + Ok(None) +} + +#[plugin_hook(CompilationAfterProcessAssets for ShareUsagePlugin)] +async fn after_process_assets(&self, compilation: &mut Compilation) -> Result<()> { + // Generate ONLY the local usage data and emit as asset + // This represents what THIS application uses from shared modules + let usage_data = self.analyze_consume_shared_usage(compilation); + + let report = ShareUsageReport { + tree_shake: usage_data, + }; + + let content = serde_json::to_string_pretty(&report) + .map_err(|e| Error::msg(format!("Failed to serialize share usage report: {e}")))?; + + let filename = &self.options.filename; + + // Always emit the share-usage.json as a build asset + compilation.assets_mut().insert( + filename.clone(), + CompilationAsset::new(Some(RawSource::from(content).boxed()), AssetInfo::default()), + ); + + Ok(()) +} + +#[async_trait] +impl Plugin for ShareUsagePlugin { + fn name(&self) -> &'static str { + "ShareUsagePlugin" + } + + fn apply(&self, ctx: &mut ApplyContext) -> Result<()> { + // Hook into optimize_dependencies to provide data to FlagDependencyUsagePlugin + ctx + .compilation_hooks + .optimize_dependencies + .tap(optimize_dependencies::new(self)); + + // Still generate the report file for debugging/external tools + ctx + .compilation_hooks + .after_process_assets + .tap(after_process_assets::new(self)); + Ok(()) + } +} diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/README.md b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/README.md new file mode 100644 index 000000000000..282bc18d9104 --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/README.md @@ -0,0 +1,42 @@ +# Module Federation Tree-Shaking with External Usage Test + +This test validates that Module Federation can properly tree-shake shared modules while preserving exports needed by external systems. + +## Test Structure + +- **module.js**: Exports multiple values to test different scenarios: + - **Directly used**: `used`, `alsoUsed`, `usedBoth` - imported and used in tests + - **Unused**: `unused`, `alsoUnused`, `neverUsed` - should be tree-shaken + - **Externally preserved**: `externallyUsed1`, `externallyUsed2`, `sharedUtility` - preserved via external-usage.json + +- **external-usage.json**: Specifies exports to preserve for external systems using share keys + +- **bootstrap.js**: The main test file that: + - Uses `used`, `alsoUsed`, and `usedBoth` directly + - Verifies that unused exports are tree-shaken (undefined) + - Verifies that externally marked exports are preserved despite not being directly used + +- **index.js**: Entry point that dynamically imports bootstrap.js (avoids eager loading) + +## How It Works + +1. The ModuleFederationPlugin automatically applies ShareUsagePlugin +2. ShareUsagePlugin analyzes what THIS app uses from shared modules +3. It loads `external-usage.json` to see what OTHER apps need preserved +4. Merges both into `share-usage.json`: + - Local usage: exports this app actually imports (marked true) + - External requirements: exports other apps need (also marked true) + - Unused by anyone: safe to tree-shake (marked false) +5. FlagDependencyUsagePlugin reads `share-usage.json` during optimization +6. Tree-shaking preserves all exports marked as true, removes those marked as false + +## Expected Behavior + +**Available exports (✅):** +- `used`, `alsoUsed`, `usedBoth` - directly imported +- `externallyUsed1`, `externallyUsed2`, `sharedUtility` - preserved via external-usage.json + +**Tree-shaken exports (❌):** +- `unused`, `alsoUnused`, `neverUsed` - not used anywhere and not marked for external preservation + +This ensures that Module Federation builds can safely tree-shake while maintaining compatibility with external consumers. \ No newline at end of file diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/bootstrap.js b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/bootstrap.js new file mode 100644 index 000000000000..c167dfacc7ff --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/bootstrap.js @@ -0,0 +1,41 @@ +const mod = require("./module"); +const collectedShares = __non_webpack_require__('./share-usage.json') + +// Actually use the exports so they're marked as used +const locallyUsed = mod.used; +const alsoLocallyUsed = mod.alsoUsed; +const usedInBothPlaces = mod.usedBoth; + +it("should tree shake unused exports in shared modules", () => { + // Directly used exports - should be available + expect(locallyUsed).toBe(42); + expect(alsoLocallyUsed).toBe("directly imported"); + expect(usedInBothPlaces).toBe("used locally and externally"); + + // Unused exports - should be tree-shaken (undefined) + expect(mod.unused).toBe(undefined); + expect(mod.alsoUnused).toBe(undefined); + expect(mod.neverUsed).toBe(undefined); + + // Externally preserved exports (via external-usage.json) - should be available + expect(mod.externallyUsed1).toBe("preserved for remote-app"); + expect(mod.externallyUsed2).toBe("preserved for analytics"); + expect(typeof mod.sharedUtility).toBe("function"); + expect(mod.sharedUtility()).toBe("external system needs this"); + + // Assert share-usage.json correctly tracks local usage + expect(collectedShares.treeShake.module).toBeDefined(); + + // Locally used exports should be marked as true + expect(collectedShares.treeShake.module.used).toBe(true); + expect(collectedShares.treeShake.module.alsoUsed).toBe(true); + expect(collectedShares.treeShake.module.usedBoth).toBe(true); + + // Locally unused exports should be marked as false (even if preserved externally) + expect(collectedShares.treeShake.module.unused).toBe(false); + expect(collectedShares.treeShake.module.alsoUnused).toBe(false); + expect(collectedShares.treeShake.module.neverUsed).toBe(false); + expect(collectedShares.treeShake.module.externallyUsed1).toBe(false); + expect(collectedShares.treeShake.module.externallyUsed2).toBe(false); + expect(collectedShares.treeShake.module.sharedUtility).toBe(false); +}); diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/external-usage.json b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/external-usage.json new file mode 100644 index 000000000000..4d353ba75b69 --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/external-usage.json @@ -0,0 +1,15 @@ +{ + "treeShake": { + "module": { + "externallyUsed1": true, + "externallyUsed2": true, + "sharedUtility": true, + "usedBoth": true, + "unused": false, + "alsoUnused": false, + "neverUsed": false, + "used": false, + "alsoUsed": false + } + } +} \ No newline at end of file diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/index.js b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/index.js new file mode 100644 index 000000000000..50599f7ad8ed --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/index.js @@ -0,0 +1 @@ +import("./bootstrap"); diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/module.js b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/module.js new file mode 100644 index 000000000000..d71c515a6d28 --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/module.js @@ -0,0 +1,16 @@ +// Directly used exports +export const used = 42; +export const alsoUsed = "directly imported"; + +// Unused exports (should be tree-shaken) +export const unused = 1; +export const alsoUnused = "not imported anywhere"; +export const neverUsed = { value: "tree-shaken" }; + +// Externally preserved exports (not directly used but kept via external-usage.json) +export const externallyUsed1 = "preserved for remote-app"; +export const externallyUsed2 = "preserved for analytics"; +export const sharedUtility = () => "external system needs this"; + +// Mixed case - used locally AND externally marked +export const usedBoth = "used locally and externally"; diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/rspack.config.js b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/rspack.config.js new file mode 100644 index 000000000000..f4f4b88d0a9c --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/rspack.config.js @@ -0,0 +1,37 @@ +const { ModuleFederationPlugin } = require("@rspack/core").container; + +/** + * This test demonstrates tree-shaking with Module Federation and external usage preservation. + * + * The ShareUsagePlugin (automatically applied by ModuleFederationPlugin) will: + * 1. Analyze which exports are used locally + * 2. Load external-usage.json to see which exports are needed by external systems + * 3. Generate share-usage.json with combined usage information + * 4. FlagDependencyUsagePlugin reads this to preserve necessary exports during tree-shaking + * + * @type {import("@rspack/core").Configuration} + */ +module.exports = { + entry: './index.js', + mode: "production", + optimization: { + usedExports: true, + sideEffects: false, + }, + plugins: [ + new ModuleFederationPlugin({ + name: "app", + filename: "remoteEntry.js", + exposes: { + "./module": "./module", + }, + shared: { + "./module": { + shareKey: "module", + version: "1.0.0", + singleton: true, + }, + }, + }), + ], +}; diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/share-usage.json b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/share-usage.json new file mode 100644 index 000000000000..e612f16e3fb2 --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/share-usage.json @@ -0,0 +1,15 @@ +{ + "treeShake": { + "module": { + "sharedUtility": false, + "unused": false, + "alsoUnused": false, + "alsoUsed": false, + "usedBoth": false, + "externallyUsed1": false, + "used": false, + "neverUsed": false, + "externallyUsed2": false + } + } +} \ No newline at end of file diff --git a/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/webpack.config.js b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/webpack.config.js new file mode 120000 index 000000000000..746f47c2e2cf --- /dev/null +++ b/tests/webpack-test/configCases/sharing/consume-shared-tree-shaking/webpack.config.js @@ -0,0 +1 @@ +rspack.config.js \ No newline at end of file