From 605695224c235efbf3f6b086218a2980b366ef47 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 17 Dec 2024 21:41:33 -0800 Subject: [PATCH 1/2] fix(enhanced): pass layer information through factory and dependency --- packages/enhanced/src/lib/sharing/ProvideSharedModule.ts | 1 + .../enhanced/src/lib/sharing/ProvideSharedModuleFactory.ts | 1 + packages/enhanced/src/lib/sharing/ProvideSharedPlugin.ts | 1 + packages/enhanced/src/lib/sharing/utils.ts | 4 +++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/enhanced/src/lib/sharing/ProvideSharedModule.ts b/packages/enhanced/src/lib/sharing/ProvideSharedModule.ts index 64130b181b3..8558c8a0b16 100644 --- a/packages/enhanced/src/lib/sharing/ProvideSharedModule.ts +++ b/packages/enhanced/src/lib/sharing/ProvideSharedModule.ts @@ -212,6 +212,7 @@ class ProvideSharedModule extends Module { requiredVersion: this._requiredVersion, strictVersion: this._strictVersion, singleton: this._singleton, + layer: this.layer }, }); return { sources, data, runtimeRequirements }; diff --git a/packages/enhanced/src/lib/sharing/ProvideSharedModuleFactory.ts b/packages/enhanced/src/lib/sharing/ProvideSharedModuleFactory.ts index ac9cb5959f9..4f90fba9af8 100644 --- a/packages/enhanced/src/lib/sharing/ProvideSharedModuleFactory.ts +++ b/packages/enhanced/src/lib/sharing/ProvideSharedModuleFactory.ts @@ -39,6 +39,7 @@ class ProvideSharedModuleFactory extends ModuleFactory { dep.requiredVersion, dep.strictVersion, dep.singleton, + dep.layer ), }); } diff --git a/packages/enhanced/src/lib/sharing/ProvideSharedPlugin.ts b/packages/enhanced/src/lib/sharing/ProvideSharedPlugin.ts index ee7270df890..9c0349facfd 100644 --- a/packages/enhanced/src/lib/sharing/ProvideSharedPlugin.ts +++ b/packages/enhanced/src/lib/sharing/ProvideSharedPlugin.ts @@ -265,6 +265,7 @@ class ProvideSharedPlugin { config.requiredVersion!, config.strictVersion!, config.singleton!, + config.layer ), { name: undefined, diff --git a/packages/enhanced/src/lib/sharing/utils.ts b/packages/enhanced/src/lib/sharing/utils.ts index 2c433224599..129b6130697 100644 --- a/packages/enhanced/src/lib/sharing/utils.ts +++ b/packages/enhanced/src/lib/sharing/utils.ts @@ -4,7 +4,7 @@ */ import { isRequiredVersion } from '@module-federation/sdk'; -import type { ConsumeOptions } from 'webpack/lib/sharing/ConsumeSharedModule'; +import type { ConsumeOptions } from '../../declarations/plugins/sharing/ConsumeSharedModule'; import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; import type { InputFileSystem } from 'webpack/lib/util/fs'; const { join, dirname, readJson } = require( @@ -459,6 +459,7 @@ export function normalizeConsumeShareOptions(consumeOptions: ConsumeOptions) { eager, shareKey, shareScope, + layer, } = consumeOptions; return { shareConfig: { @@ -467,6 +468,7 @@ export function normalizeConsumeShareOptions(consumeOptions: ConsumeOptions) { strictVersion, singleton, eager, + layer, }, shareScope, shareKey, From 8f94b549274a8dd4943b53e2833f05137a7125d4 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 17 Dec 2024 22:49:21 -0800 Subject: [PATCH 2/2] feat(enhanced): end to end layer support --- .../src/lib/sharing/ShareRuntimeModule.ts | 61 ++++++++-- .../container/3-layers-full/App.js | 7 ++ .../container/3-layers-full/ComponentA.js | 5 + .../3-layers-full/ComponentALayers.js | 6 + .../container/3-layers-full/index.js | 15 +++ .../3-layers-full/layered-react-loader.js | 4 + .../3-layers-full/node_modules/react.js | 4 + .../container/3-layers-full/package.json | 11 ++ .../container/3-layers-full/test.config.js | 5 + .../container/3-layers-full/upgrade-react.js | 5 + .../container/3-layers-full/webpack.config.js | 104 ++++++++++++++++++ .../container/4-layers-full/App.js | 10 ++ .../container/4-layers-full/ComponentB.js | 5 + .../container/4-layers-full/ComponentC.js | 7 ++ .../container/4-layers-full/index.js | 15 +++ .../4-layers-full/node_modules/package.json | 3 + .../4-layers-full/node_modules/react.js | 3 + .../container/4-layers-full/package.json | 9 ++ .../container/4-layers-full/test.config.js | 5 + .../container/4-layers-full/upgrade-react.js | 5 + .../container/4-layers-full/webpack.config.js | 65 +++++++++++ 21 files changed, 344 insertions(+), 10 deletions(-) create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/App.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/ComponentA.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/ComponentALayers.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/index.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/layered-react-loader.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/node_modules/react.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/package.json create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/test.config.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/upgrade-react.js create mode 100644 packages/enhanced/test/configCases/container/3-layers-full/webpack.config.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/App.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/ComponentB.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/ComponentC.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/index.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/node_modules/package.json create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/node_modules/react.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/package.json create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/test.config.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/upgrade-react.js create mode 100644 packages/enhanced/test/configCases/container/4-layers-full/webpack.config.js diff --git a/packages/enhanced/src/lib/sharing/ShareRuntimeModule.ts b/packages/enhanced/src/lib/sharing/ShareRuntimeModule.ts index b7bf110e13b..510b8dfd4f4 100644 --- a/packages/enhanced/src/lib/sharing/ShareRuntimeModule.ts +++ b/packages/enhanced/src/lib/sharing/ShareRuntimeModule.ts @@ -74,10 +74,14 @@ class ShareRuntimeModule extends RuntimeModule { if (sharedOption) { sharedInitOptions[sharedOption.name] = sharedInitOptions[sharedOption.name] || []; - const isSameVersion = sharedInitOptions[sharedOption.name].find( - (s) => s.version === sharedOption.version, + const isSameVersionAndLayer = sharedInitOptions[ + sharedOption.name + ].find( + (s) => + s.version === sharedOption.version && + s.shareConfig?.layer === sharedOption.shareConfig?.layer, ); - if (!isSameVersion) { + if (!isSameVersionAndLayer) { sharedInitOptions[sharedOption.name].push(sharedOption); } } @@ -88,18 +92,19 @@ class ShareRuntimeModule extends RuntimeModule { (sum, sharedName) => { const sharedOptions = sharedInitOptions[sharedName]; let str = ''; - sharedOptions.forEach((sharedOption) => { + + // Ensure all options are included without filtering + sharedOptions.forEach((option) => { str += `{${Template.indent([ - `version: ${sharedOption.version},`, - `get: ${sharedOption.getter},`, - `scope: ${JSON.stringify(sharedOption.shareScope)},`, - `shareConfig: ${JSON.stringify(sharedOption.shareConfig)}`, + `version: ${option.version},`, + `get: ${option.getter},`, + `scope: ${JSON.stringify(option.shareScope)},`, + `shareConfig: ${JSON.stringify(option.shareConfig)}`, ])}},`; }); - str = `[${str}]`; + str = `[${str}]`; sum += `${Template.indent([`"${sharedName}": ${str},`])}`; - return sum; }, '', @@ -108,6 +113,42 @@ class ShareRuntimeModule extends RuntimeModule { const federationGlobal = getFederationGlobalScope( RuntimeGlobals || ({} as typeof RuntimeGlobals), ); + + // Group shared modules by scope and layer + const scopedModules = new Map< + string, + Map> + >(); + for (const [scopeName, stages] of initCodePerScope) { + const layeredModules = new Map>(); + scopedModules.set(scopeName, layeredModules); + + for (const [, inits] of stages) { + for (const init of inits) { + const layer = init.match(/layer:\s*["']([^"']+)["']/)?.[1]; + let moduleSet = layeredModules.get(layer); + if (!moduleSet) { + moduleSet = new Set(); + layeredModules.set(layer, moduleSet); + } + moduleSet.add(init); + } + } + } + + // Generate the registration code + const registrationCode = Array.from(scopedModules.entries()) + .map(([scopeName, layeredModules]) => { + const cases = Array.from(layeredModules.entries()) + .map(([layer, inits]) => { + const initCode = Array.from(inits).join('\n'); + return `case "${scopeName}": {\n${Template.indent(initCode)}\n}`; + }) + .join('\nbreak;\n'); + return cases; + }) + .join('\n'); + return Template.asString([ `${getFederationGlobalScope( RuntimeGlobals, diff --git a/packages/enhanced/test/configCases/container/3-layers-full/App.js b/packages/enhanced/test/configCases/container/3-layers-full/App.js new file mode 100644 index 00000000000..3a22bcfa277 --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/App.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +import ComponentALayers from 'containerA/ComponentALayers'; + +export default () => { + return `App rendered with [${React()}], [${ComponentA()}] and [${ComponentALayers()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/3-layers-full/ComponentA.js b/packages/enhanced/test/configCases/container/3-layers-full/ComponentA.js new file mode 100644 index 00000000000..0e5b6e1ed71 --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentA rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/3-layers-full/ComponentALayers.js b/packages/enhanced/test/configCases/container/3-layers-full/ComponentALayers.js new file mode 100644 index 00000000000..18ec11750e1 --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/ComponentALayers.js @@ -0,0 +1,6 @@ +import * as React from 'react'; + +export default () => { + debugger; + return `ComponentALayers rendered with [${React.layeredComponentsReact()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/3-layers-full/index.js b/packages/enhanced/test/configCases/container/3-layers-full/index.js new file mode 100644 index 00000000000..0bc5492da40 --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/index.js @@ -0,0 +1,15 @@ +it('should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 0.1.2], [ComponentA rendered with [This is react 0.1.2]] and [ComponentALayers rendered with [This is layered react]]', + ); + return import('./upgrade-react').then(({ default: upgrade }) => { + upgrade(); + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 1.2.3], [ComponentA rendered with [This is react 1.2.3]] and [ComponentALayers rendered with [This is layered react]]', + ); + }); + }); +}); diff --git a/packages/enhanced/test/configCases/container/3-layers-full/layered-react-loader.js b/packages/enhanced/test/configCases/container/3-layers-full/layered-react-loader.js new file mode 100644 index 00000000000..964fbb66ef1 --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/layered-react-loader.js @@ -0,0 +1,4 @@ +module.exports = function (source) { + console.log(source); + return source.replace('__PLACEHOLDER__', 'This is layered react'); +}; diff --git a/packages/enhanced/test/configCases/container/3-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/container/3-layers-full/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/node_modules/react.js @@ -0,0 +1,4 @@ +let version = "0.1.2"; +export default () => `This is react ${version}`; +export function setVersion(v) { version = v; } +export const layeredComponentsReact = () => "__PLACEHOLDER__"; diff --git a/packages/enhanced/test/configCases/container/3-layers-full/package.json b/packages/enhanced/test/configCases/container/3-layers-full/package.json new file mode 100644 index 00000000000..4e44b5b102f --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/package.json @@ -0,0 +1,11 @@ +{ + "name": "3-layers-full", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "webpack --config=webpack.config.js" + }, + "dependencies": { + "react": "1.0.0" + } +} diff --git a/packages/enhanced/test/configCases/container/3-layers-full/test.config.js b/packages/enhanced/test/configCases/container/3-layers-full/test.config.js new file mode 100644 index 00000000000..861157bc4ed --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return i === 0 ? './main.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/container/3-layers-full/upgrade-react.js b/packages/enhanced/test/configCases/container/3-layers-full/upgrade-react.js new file mode 100644 index 00000000000..5bf08a67d5a --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/upgrade-react.js @@ -0,0 +1,5 @@ +import { setVersion } from 'react'; + +export default function upgrade() { + setVersion('1.2.3'); +} diff --git a/packages/enhanced/test/configCases/container/3-layers-full/webpack.config.js b/packages/enhanced/test/configCases/container/3-layers-full/webpack.config.js new file mode 100644 index 00000000000..4220ac2db27 --- /dev/null +++ b/packages/enhanced/test/configCases/container/3-layers-full/webpack.config.js @@ -0,0 +1,104 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); +const path = require('path'); + +const common = { + name: 'layer_container', + exposes: { + './ComponentA': { + import: './ComponentA', + }, + './ComponentALayers': { + import: './ComponentALayers', + }, + }, + shared: { + react: { + version: false, + requiredVersion: false, + singleton: true, + }, + 'layered-react': { + request: 'react', + import: 'react', + shareKey: 'react', + version: false, + requiredVersion: false, + singleton: true, + layer: 'layered-components', + issuerLayer: 'layered-components', + }, + }, +}; + +const commonConfig = { + devtool: false, + experiments: { + layers: true, + }, + entry: './index.js', + mode: 'development', + module: { + rules: [ + { + test: /ComponentALayers\.js$/, + layer: 'layered-components', + }, + { + test: /react$/, + issuerLayer: 'layered-components', + layer: 'layered-components', + use: [ + { + loader: path.resolve(__dirname, './layered-react-loader.js'), + }, + ], + }, + ], + }, +}; + +module.exports = [ + { + ...commonConfig, + output: { + filename: '[name].js', + uniqueName: '3-layers-full', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'commonjs-module' }, + filename: 'container.js', + remotes: { + containerA: { + external: './container.js', + }, + }, + ...common, + }), + ], + }, + { + ...commonConfig, + experiments: { + ...commonConfig.experiments, + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: '3-layers-full-mjs', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: { + external: './container.mjs', + }, + }, + ...common, + }), + ], + target: 'node14', + }, +]; diff --git a/packages/enhanced/test/configCases/container/4-layers-full/App.js b/packages/enhanced/test/configCases/container/4-layers-full/App.js new file mode 100644 index 00000000000..40ef934441f --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/App.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +import ComponentB from 'containerB/ComponentB'; +import LocalComponentB from './ComponentB'; + +export default () => { + return `App rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; + +expect(ComponentB).not.toBe(LocalComponentB); diff --git a/packages/enhanced/test/configCases/container/4-layers-full/ComponentB.js b/packages/enhanced/test/configCases/container/4-layers-full/ComponentB.js new file mode 100644 index 00000000000..bd88caedbb0 --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/ComponentB.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentB rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/4-layers-full/ComponentC.js b/packages/enhanced/test/configCases/container/4-layers-full/ComponentC.js new file mode 100644 index 00000000000..6e6fea21c9b --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/ComponentC.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +import ComponentB from 'containerB/ComponentB'; + +export default () => { + return `ComponentC rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/4-layers-full/index.js b/packages/enhanced/test/configCases/container/4-layers-full/index.js new file mode 100644 index 00000000000..81ffe90a07f --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/index.js @@ -0,0 +1,15 @@ +it('should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 2.1.0] and [ComponentA rendered with [This is react 2.1.0]] and [ComponentB rendered with [This is react 2.1.0]]', + ); + return import('./upgrade-react').then(({ default: upgrade }) => { + upgrade(); + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 3.2.1] and [ComponentA rendered with [This is react 3.2.1]] and [ComponentB rendered with [This is react 3.2.1]]', + ); + }); + }); +}); diff --git a/packages/enhanced/test/configCases/container/4-layers-full/node_modules/package.json b/packages/enhanced/test/configCases/container/4-layers-full/node_modules/package.json new file mode 100644 index 00000000000..87032da008a --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/node_modules/package.json @@ -0,0 +1,3 @@ +{ + "version": "2.1.0" +} diff --git a/packages/enhanced/test/configCases/container/4-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/container/4-layers-full/node_modules/react.js new file mode 100644 index 00000000000..97d35a4bc9c --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/node_modules/react.js @@ -0,0 +1,3 @@ +let version = "2.1.0"; +export default () => `This is react ${version}`; +export function setVersion(v) { version = v; } diff --git a/packages/enhanced/test/configCases/container/4-layers-full/package.json b/packages/enhanced/test/configCases/container/4-layers-full/package.json new file mode 100644 index 00000000000..be6238fec84 --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "engines": { + "node": ">=10.13.0" + }, + "dependencies": { + "react": "*" + } +} diff --git a/packages/enhanced/test/configCases/container/4-layers-full/test.config.js b/packages/enhanced/test/configCases/container/4-layers-full/test.config.js new file mode 100644 index 00000000000..861157bc4ed --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return i === 0 ? './main.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/container/4-layers-full/upgrade-react.js b/packages/enhanced/test/configCases/container/4-layers-full/upgrade-react.js new file mode 100644 index 00000000000..fd400f3d5a3 --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/upgrade-react.js @@ -0,0 +1,5 @@ +import { setVersion } from 'react'; + +export default function upgrade() { + setVersion('3.2.1'); +} diff --git a/packages/enhanced/test/configCases/container/4-layers-full/webpack.config.js b/packages/enhanced/test/configCases/container/4-layers-full/webpack.config.js new file mode 100644 index 00000000000..23ebf0f82ad --- /dev/null +++ b/packages/enhanced/test/configCases/container/4-layers-full/webpack.config.js @@ -0,0 +1,65 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +const common = { + entry: { + main: './index.js', + }, + optimization: { + runtimeChunk: 'single', + }, +}; + +const commonMF = { + runtime: false, + exposes: { + './ComponentB': './ComponentB', + './ComponentC': './ComponentC', + }, + shared: ['react'], +}; + +/** @type {import("../../../../").Configuration[]} */ +module.exports = [ + { + ...common, + output: { + filename: '[name].js', + uniqueName: '4-layers-full', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'layers_container_2', + library: { type: 'commonjs-module' }, + filename: 'container.js', + remotes: { + containerA: '../3-layers-full/container.js', + containerB: './container.js', + }, + ...commonMF, + }), + ], + }, + { + ...common, + experiments: { + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: '4-layers-full-mjs', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'layers_container_2', + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: '../../3-layers-full/module/container.mjs', + containerB: './container.mjs', + }, + ...commonMF, + }), + ], + target: 'node14', + }, +];