diff --git a/.cursorignore b/.cursorignore index 4a2b9586d04..610b1fe7f68 100644 --- a/.cursorignore +++ b/.cursorignore @@ -7,10 +7,19 @@ packages/core packages/utilities packages/typescript packages/native-* +packages/* +!packages/runtime apps **/configCases +!**/configCases/container **/dist apps/** *.snap *.js - +webpack/* +tools/* +.github +.vscode +*.py +.husky +.changeset diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000000..63af60d8396 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,11 @@ +for layers implementation: + +- shareConfig should contain layer key / value, it may be undefiend if there is none set by user. there should be no issuerLayer in the runtime package +- the layer should be composited into shareScope as layer ? (layer)+shareScope : shareScope +- for resolving layers, we should reconstruct the composite scope then search for the version +- do not remove asserts +- do not delete code or comments +- do not add or remove early returns from the function flow + + +to run tests execute "pnpm runtime:test" diff --git a/.gitignore b/.gitignore index b93b31dd700..b22473c18d4 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ apps/**/dist # test cases !packages/enhanced/test/configCases/**/**/node_modules -packages/enhanced/test/js +packages/enhanced/test/js/** .ignored **/.mf **/.mf/** diff --git a/package.json b/package.json index a10717fd65a..2b6ca7a8df6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "docs": "typedoc", "f": "nx format:write", "enhanced:jest": "pnpm build && cd packages/enhanced && NODE_OPTIONS=--experimental-vm-modules npx jest test/ConfigTestCases.basictest.js", + "runtime:test": "pnpm build && nx run runtime:test", "lint": "nx run-many --target=lint", "test": "nx run-many --target=test", "build": "nx run-many --target=build --parallel=5 --projects=tag:type:pkg", diff --git a/packages/bridge/vue3-bridge/vite.config.ts.timestamp-1735016656823-c2cfc29358244.mjs b/packages/bridge/vue3-bridge/vite.config.ts.timestamp-1735016656823-c2cfc29358244.mjs new file mode 100644 index 00000000000..3917980df34 --- /dev/null +++ b/packages/bridge/vue3-bridge/vite.config.ts.timestamp-1735016656823-c2cfc29358244.mjs @@ -0,0 +1,91 @@ +// vite.config.ts +import { defineConfig } from 'file:///Users/bytedance/dev/universe/node_modules/.pnpm/vite@5.2.14_@types+node@18.16.9_less@4.2.0_stylus@0.64.0/node_modules/vite/dist/node/index.js'; +import vue from 'file:///Users/bytedance/dev/universe/node_modules/.pnpm/@vitejs+plugin-vue@5.1.4_vite@5.2.14_vue@3.5.10/node_modules/@vitejs/plugin-vue/dist/index.mjs'; +import path from 'path'; +import dts from 'file:///Users/bytedance/dev/universe/node_modules/.pnpm/vite-plugin-dts@4.3.0_@types+node@18.16.9_rollup@4.24.0_typescript@5.5.2_vite@5.2.14/node_modules/vite-plugin-dts/dist/index.mjs'; +import vueJsx from 'file:///Users/bytedance/dev/universe/node_modules/.pnpm/@vitejs+plugin-vue-jsx@4.0.1_vite@5.2.14_vue@3.5.10/node_modules/@vitejs/plugin-vue-jsx/dist/index.mjs'; + +// package.json +var package_default = { + name: '@module-federation/bridge-vue3', + author: 'zhouxiao ', + license: 'MIT', + repository: { + type: 'git', + url: 'https://github.com/module-federation/core', + directory: 'packages/vue3-bridge', + }, + version: '0.8.3', + publishConfig: { + access: 'public', + }, + type: 'module', + main: './dist/index.cjs.js', + module: './dist/index.es.js', + types: './dist/index.d.ts', + files: [ + 'dist/', + 'src/', + 'CHANGELOG.md', + 'LICENSE', + 'package.json', + 'project.json', + 'README.md', + 'tsconfig.json', + 'tsconfig.node.json', + 'vite.config.ts', + ], + scripts: { + dev: 'vite', + build: 'vite build', + preview: 'vite preview', + }, + peerDependencies: { + vue: '=3', + 'vue-router': '=3', + }, + dependencies: { + '@module-federation/bridge-shared': 'workspace:*', + '@module-federation/sdk': 'workspace:*', + '@module-federation/runtime': 'workspace:*', + }, + devDependencies: { + '@vitejs/plugin-vue': '^5.0.4', + '@vitejs/plugin-vue-jsx': '^4.0.0', + typescript: '^5.2.2', + vite: '^5.2.14', + 'vite-plugin-dts': '^4.3.0', + vue: '^3.4.21', + 'vue-router': '4.4.5', + 'vue-tsc': '^2.0.6', + }, +}; + +// vite.config.ts +var __vite_injected_original_dirname = + '/Users/bytedance/dev/universe/packages/bridge/vue3-bridge'; +var vite_config_default = defineConfig({ + plugins: [ + vue(), + dts({ + rollupTypes: true, + bundledPackages: ['@module-federation/bridge-shared'], + }), + vueJsx(), + ], + build: { + lib: { + entry: path.resolve(__vite_injected_original_dirname, 'src/index.ts'), + formats: ['cjs', 'es'], + fileName: (format) => `index.${format}.js`, + }, + rollupOptions: { + external: ['vue', 'vue-router'], + }, + }, + define: { + __APP_VERSION__: JSON.stringify(package_default.version), + }, +}); +export { vite_config_default as default }; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAicGFja2FnZS5qc29uIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL2J5dGVkYW5jZS9kZXYvdW5pdmVyc2UvcGFja2FnZXMvYnJpZGdlL3Z1ZTMtYnJpZGdlXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvYnl0ZWRhbmNlL2Rldi91bml2ZXJzZS9wYWNrYWdlcy9icmlkZ2UvdnVlMy1icmlkZ2Uvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL2J5dGVkYW5jZS9kZXYvdW5pdmVyc2UvcGFja2FnZXMvYnJpZGdlL3Z1ZTMtYnJpZGdlL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XG5pbXBvcnQgdnVlIGZyb20gJ0B2aXRlanMvcGx1Z2luLXZ1ZSc7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCBkdHMgZnJvbSAndml0ZS1wbHVnaW4tZHRzJztcbmltcG9ydCB2dWVKc3ggZnJvbSAnQHZpdGVqcy9wbHVnaW4tdnVlLWpzeCc7XG5pbXBvcnQgcGFja2FnZUpzb24gZnJvbSAnLi9wYWNrYWdlLmpzb24nO1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgdnVlKCksXG4gICAgZHRzKHtcbiAgICAgIHJvbGx1cFR5cGVzOiB0cnVlLFxuICAgICAgYnVuZGxlZFBhY2thZ2VzOiBbJ0Btb2R1bGUtZmVkZXJhdGlvbi9icmlkZ2Utc2hhcmVkJ10sXG4gICAgfSksXG4gICAgdnVlSnN4KCksXG4gIF0sXG4gIGJ1aWxkOiB7XG4gICAgbGliOiB7XG4gICAgICBlbnRyeTogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJ3NyYy9pbmRleC50cycpLFxuICAgICAgZm9ybWF0czogWydjanMnLCAnZXMnXSxcbiAgICAgIGZpbGVOYW1lOiAoZm9ybWF0KSA9PiBgaW5kZXguJHtmb3JtYXR9LmpzYCxcbiAgICB9LFxuICAgIHJvbGx1cE9wdGlvbnM6IHtcbiAgICAgIGV4dGVybmFsOiBbJ3Z1ZScsICd2dWUtcm91dGVyJ10sXG4gICAgfSxcbiAgfSxcbiAgZGVmaW5lOiB7XG4gICAgX19BUFBfVkVSU0lPTl9fOiBKU09OLnN0cmluZ2lmeShwYWNrYWdlSnNvbi52ZXJzaW9uKSxcbiAgfSxcbn0pO1xuIiwgIntcbiAgXCJuYW1lXCI6IFwiQG1vZHVsZS1mZWRlcmF0aW9uL2JyaWRnZS12dWUzXCIsXG4gIFwiYXV0aG9yXCI6IFwiemhvdXhpYW8gPGNvZGluZ3p4QGdtYWlsLmNvbT5cIixcbiAgXCJsaWNlbnNlXCI6IFwiTUlUXCIsXG4gIFwicmVwb3NpdG9yeVwiOiB7XG4gICAgXCJ0eXBlXCI6IFwiZ2l0XCIsXG4gICAgXCJ1cmxcIjogXCJodHRwczovL2dpdGh1Yi5jb20vbW9kdWxlLWZlZGVyYXRpb24vY29yZVwiLFxuICAgIFwiZGlyZWN0b3J5XCI6IFwicGFja2FnZXMvdnVlMy1icmlkZ2VcIlxuICB9LFxuICBcInZlcnNpb25cIjogXCIwLjguM1wiLFxuICBcInB1Ymxpc2hDb25maWdcIjoge1xuICAgIFwiYWNjZXNzXCI6IFwicHVibGljXCJcbiAgfSxcbiAgXCJ0eXBlXCI6IFwibW9kdWxlXCIsXG4gIFwibWFpblwiOiBcIi4vZGlzdC9pbmRleC5janMuanNcIixcbiAgXCJtb2R1bGVcIjogXCIuL2Rpc3QvaW5kZXguZXMuanNcIixcbiAgXCJ0eXBlc1wiOiBcIi4vZGlzdC9pbmRleC5kLnRzXCIsXG4gIFwiZmlsZXNcIjogW1xuICAgIFwiZGlzdC9cIixcbiAgICBcInNyYy9cIixcbiAgICBcIkNIQU5HRUxPRy5tZFwiLFxuICAgIFwiTElDRU5TRVwiLFxuICAgIFwicGFja2FnZS5qc29uXCIsXG4gICAgXCJwcm9qZWN0Lmpzb25cIixcbiAgICBcIlJFQURNRS5tZFwiLFxuICAgIFwidHNjb25maWcuanNvblwiLFxuICAgIFwidHNjb25maWcubm9kZS5qc29uXCIsXG4gICAgXCJ2aXRlLmNvbmZpZy50c1wiXG4gIF0sXG4gIFwic2NyaXB0c1wiOiB7XG4gICAgXCJkZXZcIjogXCJ2aXRlXCIsXG4gICAgXCJidWlsZFwiOiBcInZpdGUgYnVpbGRcIixcbiAgICBcInByZXZpZXdcIjogXCJ2aXRlIHByZXZpZXdcIlxuICB9LFxuICBcInBlZXJEZXBlbmRlbmNpZXNcIjoge1xuICAgIFwidnVlXCI6IFwiPTNcIixcbiAgICBcInZ1ZS1yb3V0ZXJcIjogXCI9M1wiXG4gIH0sXG4gIFwiZGVwZW5kZW5jaWVzXCI6IHtcbiAgICBcIkBtb2R1bGUtZmVkZXJhdGlvbi9icmlkZ2Utc2hhcmVkXCI6IFwid29ya3NwYWNlOipcIixcbiAgICBcIkBtb2R1bGUtZmVkZXJhdGlvbi9zZGtcIjogXCJ3b3Jrc3BhY2U6KlwiLFxuICAgIFwiQG1vZHVsZS1mZWRlcmF0aW9uL3J1bnRpbWVcIjogXCJ3b3Jrc3BhY2U6KlwiXG4gIH0sXG4gIFwiZGV2RGVwZW5kZW5jaWVzXCI6IHtcbiAgICBcIkB2aXRlanMvcGx1Z2luLXZ1ZVwiOiBcIl41LjAuNFwiLFxuICAgIFwiQHZpdGVqcy9wbHVnaW4tdnVlLWpzeFwiOiBcIl40LjAuMFwiLFxuICAgIFwidHlwZXNjcmlwdFwiOiBcIl41LjIuMlwiLFxuICAgIFwidml0ZVwiOiBcIl41LjIuMTRcIixcbiAgICBcInZpdGUtcGx1Z2luLWR0c1wiOiBcIl40LjMuMFwiLFxuICAgIFwidnVlXCI6IFwiXjMuNC4yMVwiLFxuICAgIFwidnVlLXJvdXRlclwiOiBcIjQuNC41XCIsXG4gICAgXCJ2dWUtdHNjXCI6IFwiXjIuMC42XCJcbiAgfVxufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE2VixTQUFTLG9CQUFvQjtBQUMxWCxPQUFPLFNBQVM7QUFDaEIsT0FBTyxVQUFVO0FBQ2pCLE9BQU8sU0FBUztBQUNoQixPQUFPLFlBQVk7OztBQ0puQjtBQUFBLEVBQ0UsTUFBUTtBQUFBLEVBQ1IsUUFBVTtBQUFBLEVBQ1YsU0FBVztBQUFBLEVBQ1gsWUFBYztBQUFBLElBQ1osTUFBUTtBQUFBLElBQ1IsS0FBTztBQUFBLElBQ1AsV0FBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFNBQVc7QUFBQSxFQUNYLGVBQWlCO0FBQUEsSUFDZixRQUFVO0FBQUEsRUFDWjtBQUFBLEVBQ0EsTUFBUTtBQUFBLEVBQ1IsTUFBUTtBQUFBLEVBQ1IsUUFBVTtBQUFBLEVBQ1YsT0FBUztBQUFBLEVBQ1QsT0FBUztBQUFBLElBQ1A7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxJQUNBO0FBQUEsSUFDQTtBQUFBLElBQ0E7QUFBQSxFQUNGO0FBQUEsRUFDQSxTQUFXO0FBQUEsSUFDVCxLQUFPO0FBQUEsSUFDUCxPQUFTO0FBQUEsSUFDVCxTQUFXO0FBQUEsRUFDYjtBQUFBLEVBQ0Esa0JBQW9CO0FBQUEsSUFDbEIsS0FBTztBQUFBLElBQ1AsY0FBYztBQUFBLEVBQ2hCO0FBQUEsRUFDQSxjQUFnQjtBQUFBLElBQ2Qsb0NBQW9DO0FBQUEsSUFDcEMsMEJBQTBCO0FBQUEsSUFDMUIsOEJBQThCO0FBQUEsRUFDaEM7QUFBQSxFQUNBLGlCQUFtQjtBQUFBLElBQ2pCLHNCQUFzQjtBQUFBLElBQ3RCLDBCQUEwQjtBQUFBLElBQzFCLFlBQWM7QUFBQSxJQUNkLE1BQVE7QUFBQSxJQUNSLG1CQUFtQjtBQUFBLElBQ25CLEtBQU87QUFBQSxJQUNQLGNBQWM7QUFBQSxJQUNkLFdBQVc7QUFBQSxFQUNiO0FBQ0Y7OztBRHJEQSxJQUFNLG1DQUFtQztBQU96QyxJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsSUFDSixJQUFJO0FBQUEsTUFDRixhQUFhO0FBQUEsTUFDYixpQkFBaUIsQ0FBQyxrQ0FBa0M7QUFBQSxJQUN0RCxDQUFDO0FBQUEsSUFDRCxPQUFPO0FBQUEsRUFDVDtBQUFBLEVBQ0EsT0FBTztBQUFBLElBQ0wsS0FBSztBQUFBLE1BQ0gsT0FBTyxLQUFLLFFBQVEsa0NBQVcsY0FBYztBQUFBLE1BQzdDLFNBQVMsQ0FBQyxPQUFPLElBQUk7QUFBQSxNQUNyQixVQUFVLENBQUMsV0FBVyxTQUFTLE1BQU07QUFBQSxJQUN2QztBQUFBLElBQ0EsZUFBZTtBQUFBLE1BQ2IsVUFBVSxDQUFDLE9BQU8sWUFBWTtBQUFBLElBQ2hDO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04saUJBQWlCLEtBQUssVUFBVSxnQkFBWSxPQUFPO0FBQUEsRUFDckQ7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/packages/enhanced/src/lib/container/ContainerReferencePlugin.ts b/packages/enhanced/src/lib/container/ContainerReferencePlugin.ts index 35d0cb5710c..8b286211f79 100644 --- a/packages/enhanced/src/lib/container/ContainerReferencePlugin.ts +++ b/packages/enhanced/src/lib/container/ContainerReferencePlugin.ts @@ -131,6 +131,8 @@ class ContainerReferencePlugin { `.${data.request.slice(key.length)}`, //@ts-ignore config.shareScope, + data.contextInfo?.issuerLayer || undefined, + compiler.context ); } } diff --git a/packages/enhanced/src/lib/container/RemoteModule.ts b/packages/enhanced/src/lib/container/RemoteModule.ts index c69b7974ef7..db2b938aef4 100644 --- a/packages/enhanced/src/lib/container/RemoteModule.ts +++ b/packages/enhanced/src/lib/container/RemoteModule.ts @@ -44,19 +44,24 @@ class RemoteModule extends Module { * @param {string[]} externalRequests list of external requests to containers * @param {string} internalRequest name of exposed module in container * @param {string} shareScope the used share scope name + * @param {string=} layer optional layer name + * @param {string=} context optional context name */ constructor( request: string, externalRequests: string[], internalRequest: string, shareScope: string, + layer?: string, + context?: string ) { - super(WEBPACK_MODULE_TYPE_REMOTE); + super(WEBPACK_MODULE_TYPE_REMOTE, context, layer); this.request = request; this.externalRequests = externalRequests; this.internalRequest = internalRequest; - this.shareScope = shareScope; - this._identifier = `remote (${shareScope}) ${this.externalRequests.join( + // Compose share scope with layer if present + this.shareScope = layer ? `(${layer})${shareScope}` : shareScope; + this._identifier = `remote (${this.shareScope}) ${this.externalRequests.join( ' ', )} ${this.internalRequest}`; } @@ -106,7 +111,6 @@ class RemoteModule extends Module { * @param {function(WebpackError=): void} callback callback function * @returns {void} */ - // @ts-ignore override build( options: WebpackOptionsNormalized, compilation: Compilation, @@ -157,7 +161,6 @@ class RemoteModule extends Module { * @param {CodeGenerationContext} context context for code generation * @returns {CodeGenerationResult} result */ - // @ts-ignore override codeGeneration( context: CodeGenerationContext, ): CodeGenerationResult { @@ -176,12 +179,18 @@ class RemoteModule extends Module { ]); return { sources, data, runtimeRequirements: RUNTIME_REQUIREMENTS }; } + + /** + * Serializes the module + * @param {any} context serialization context + */ override serialize(context: any) { const { write } = context; write(this.request); write(this.externalRequests); write(this.internalRequest); write(this.shareScope); + write(this.layer); super.serialize(context); } diff --git a/packages/enhanced/src/lib/sharing/ProvideSharedModule.ts b/packages/enhanced/src/lib/sharing/ProvideSharedModule.ts index 64130b181b3..9ba3fad26f0 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..ca3e22db14c 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..bde955682d0 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/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/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, diff --git a/packages/enhanced/test/ConfigTestCases.template.js b/packages/enhanced/test/ConfigTestCases.template.js index f404a233d09..f598139154e 100644 --- a/packages/enhanced/test/ConfigTestCases.template.js +++ b/packages/enhanced/test/ConfigTestCases.template.js @@ -60,6 +60,7 @@ const describeCases = (config) => { jest.setTimeout(20000); for (const category of categories) { + if (category.name !== 'layers') continue; // eslint-disable-next-line no-loop-func describe(category.name, () => { for (const testName of category.tests) { 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/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/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', + }, +]; diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full-actual/App.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/App.js new file mode 100644 index 00000000000..3a22bcfa277 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/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/import-false/3-layers-full-actual/ComponentA.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/ComponentA.js new file mode 100644 index 00000000000..0e5b6e1ed71 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentA rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full-actual/ComponentALayers.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/ComponentALayers.js new file mode 100644 index 00000000000..18ec11750e1 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/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/import-false/3-layers-full-actual/index.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/index.js new file mode 100644 index 00000000000..d6027a53d04 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/index.js @@ -0,0 +1,8 @@ +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]]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full-actual/layered-react-loader.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/layered-react-loader.js new file mode 100644 index 00000000000..964fbb66ef1 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/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/import-false/3-layers-full-actual/layered-upgrade-react.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/layered-upgrade-react.js new file mode 100644 index 00000000000..8ae169d2e45 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/layered-upgrade-react.js @@ -0,0 +1,10 @@ +import React from 'react'; + +// This file will be processed by the layered loader +export default function initializeLayeredReactVersion() { + // Set the layered React version + React.setVersion('1.2.3'); +} + +// Initialize version immediately +initializeLayeredReactVersion(); diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full-actual/node_modules/react.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/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/import-false/3-layers-full-actual/webpack.config.js b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/webpack.config.js new file mode 100644 index 00000000000..4220ac2db27 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full-actual/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/import-false/3-layers-full/App.js b/packages/enhanced/test/configCases/import-false/3-layers-full/App.js new file mode 100644 index 00000000000..945e6682942 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full/App.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function App() { + return `App rendered with React version: [${React()}]`; +} diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full/ComponentA.js b/packages/enhanced/test/configCases/import-false/3-layers-full/ComponentA.js new file mode 100644 index 00000000000..8f29fee8d0d --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function ComponentA() { + return `ComponentA rendered with React version: [${React()}]`; +} diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full/index.js b/packages/enhanced/test/configCases/import-false/3-layers-full/index.js new file mode 100644 index 00000000000..8b2ae6e00c2 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full/index.js @@ -0,0 +1,6 @@ +it('should load App with React', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toContain('App rendered with React version:'); + }) +}); diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/import-false/3-layers-full/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/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/import-false/3-layers-full/package.json b/packages/enhanced/test/configCases/import-false/3-layers-full/package.json new file mode 100644 index 00000000000..4e44b5b102f --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/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/0-container-full/test.config.js b/packages/enhanced/test/configCases/import-false/3-layers-full/test.config.js similarity index 100% rename from packages/enhanced/test/configCases/container/0-container-full/test.config.js rename to packages/enhanced/test/configCases/import-false/3-layers-full/test.config.js diff --git a/packages/enhanced/test/configCases/import-false/3-layers-full/webpack.config.js b/packages/enhanced/test/configCases/import-false/3-layers-full/webpack.config.js new file mode 100644 index 00000000000..c0b2755c1b8 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/3-layers-full/webpack.config.js @@ -0,0 +1,28 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +module.exports = { + entry: './index.js', + mode: 'development', + devtool: false, + output: { + filename: '[name].js', + uniqueName: '3-layers-full', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'container_a', + filename: 'container.js', + library: { type: 'commonjs-module' }, + exposes: { + './ComponentA': './ComponentA', + }, + shared: { + react: { + singleton: true, + requiredVersion: false, + version: false + } + } + }), + ], +}; diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/App.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/App.js new file mode 100644 index 00000000000..40ef934441f --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/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/import-false/4-layers-full-acutal/ComponentALayers.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentALayers.js new file mode 100644 index 00000000000..a831cb9f41a --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentALayers.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function ComponentALayers() { + return `ComponentALayers (Layered React: ${React.layeredComponentsReact()})`; +} diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentB.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentB.js new file mode 100644 index 00000000000..bd88caedbb0 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentB.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentB rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentC.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/ComponentC.js new file mode 100644 index 00000000000..6e6fea21c9b --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/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/import-false/4-layers-full-acutal/index.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/index.js new file mode 100644 index 00000000000..ac3aba6166f --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/index.js @@ -0,0 +1,8 @@ +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]]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/layered-react-loader.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/layered-react-loader.js new file mode 100644 index 00000000000..f44f1f15c08 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/layered-react-loader.js @@ -0,0 +1,9 @@ +module.exports = function (source) { + console.log('Layered React Loader - Source:', source); + const transformed = source.replace( + '__PLACEHOLDER__', + 'This is layered react', + ); + console.log('Layered React Loader - Transformed:', transformed); + return transformed; +}; diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/layered-upgrade-react.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/layered-upgrade-react.js new file mode 100644 index 00000000000..8ae169d2e45 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/layered-upgrade-react.js @@ -0,0 +1,10 @@ +import React from 'react'; + +// This file will be processed by the layered loader +export default function initializeLayeredReactVersion() { + // Set the layered React version + React.setVersion('1.2.3'); +} + +// Initialize version immediately +initializeLayeredReactVersion(); diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/node_modules/package.json b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/node_modules/package.json new file mode 100644 index 00000000000..87032da008a --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/node_modules/package.json @@ -0,0 +1,3 @@ +{ + "version": "2.1.0" +} diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/node_modules/react.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/node_modules/react.js new file mode 100644 index 00000000000..97d35a4bc9c --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/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/import-false/4-layers-full-acutal/webpack.config.js b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/webpack.config.js new file mode 100644 index 00000000000..23ebf0f82ad --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full-acutal/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', + }, +]; diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full/App.js b/packages/enhanced/test/configCases/import-false/4-layers-full/App.js new file mode 100644 index 00000000000..9b231c16945 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full/App.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; + +export default function App() { + return `App rendered with React version: [${React()}]\nand remote component: [${ComponentA()}]`; +} + diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full/ComponentA.js b/packages/enhanced/test/configCases/import-false/4-layers-full/ComponentA.js new file mode 100644 index 00000000000..6061e699763 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function ComponentA() { + return `ComponentA (Regular React: ${React()})`; +} diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full/index.js b/packages/enhanced/test/configCases/import-false/4-layers-full/index.js new file mode 100644 index 00000000000..1fa3d06711e --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full/index.js @@ -0,0 +1,36 @@ +try { + if (typeof it === 'undefined') { + global.it = async function (a, b) { + return await b(); + }; + } + + if (typeof expect === 'undefined') { + global.expect = function (value) { + return { + toBe: (expected) => { + if (value !== expected) { + throw new Error(`Expected ${value} to be ${expected}`); + } + }, + toContain: (expected) => { + if (!value.includes(expected)) { + throw new Error(`Expected ${value} to contain ${expected}`); + } + }, + }; + }; + } +} catch (e) { + console.log(e); +} + +it('should load App with React and remote component', async () => { + const App = (await import('./App')).default; + const upgrade = (await import('./upgrade-react')).default; + upgrade(); + const rendered = App(); + expect(rendered).toBe( + 'App rendered with React version: [This is react 1.2.3]\nand remote component: [ComponentA rendered with React version: [This is react 1.2.3]]', + ); +}); diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/import-false/4-layers-full/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-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/1-container-full/package.json b/packages/enhanced/test/configCases/import-false/4-layers-full/package.json similarity index 100% rename from packages/enhanced/test/configCases/container/1-container-full/package.json rename to packages/enhanced/test/configCases/import-false/4-layers-full/package.json diff --git a/packages/enhanced/test/configCases/container/1-container-full/test.config.js b/packages/enhanced/test/configCases/import-false/4-layers-full/test.config.js similarity index 100% rename from packages/enhanced/test/configCases/container/1-container-full/test.config.js rename to packages/enhanced/test/configCases/import-false/4-layers-full/test.config.js diff --git a/packages/enhanced/test/configCases/import-false/4-layers-full/upgrade-react.js b/packages/enhanced/test/configCases/import-false/4-layers-full/upgrade-react.js new file mode 100644 index 00000000000..5bf08a67d5a --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-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/import-false/4-layers-full/webpack.config.js b/packages/enhanced/test/configCases/import-false/4-layers-full/webpack.config.js new file mode 100644 index 00000000000..0a0db70d3cf --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/4-layers-full/webpack.config.js @@ -0,0 +1,29 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +module.exports = { + entry: './index.js', + mode: 'development', + devtool: false, + output: { + filename: '[name].js', + uniqueName: '4-layers-full', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'container_b', + filename: 'container.js', + library: { type: 'commonjs-module' }, + remotes: { + containerA: '../3-layers-full/container.js' + }, + shared: { + react: { + singleton: true, + requiredVersion: false, + version: false, + import:false + } + } + }), + ], +}; diff --git a/packages/enhanced/test/configCases/import-false/5-layers-full/App.js b/packages/enhanced/test/configCases/import-false/5-layers-full/App.js new file mode 100644 index 00000000000..11db7ebbb9e --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-layers-full/App.js @@ -0,0 +1,5 @@ +import React, {layeredComponentsReact} from 'react'; +import ComponentA from './ComponentA'; +export default function App() { + return `App rendered with React version: [${React()}] with layer [${layeredComponentsReact()}] ${ComponentA()}`; +} diff --git a/packages/enhanced/test/configCases/import-false/5-layers-full/ComponentA.js b/packages/enhanced/test/configCases/import-false/5-layers-full/ComponentA.js new file mode 100644 index 00000000000..7aeae0c5318 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React, {layeredComponentsReact} from 'react'; + +export default function ComponentA() { + return `ComponentA rendered with React version: [${React()}] with layer [${layeredComponentsReact()}]`; +} diff --git a/packages/enhanced/test/configCases/import-false/5-layers-full/index.js b/packages/enhanced/test/configCases/import-false/5-layers-full/index.js new file mode 100644 index 00000000000..1d5291bfd1a --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-layers-full/index.js @@ -0,0 +1,6 @@ +it('should load App with React', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe('App rendered with React version: [This is react 0.1.2] with layer [This is layered react] ComponentA rendered with React version: [This is react 0.1.2] with layer [This is layered react]'); + }); +}); diff --git a/packages/enhanced/test/configCases/import-false/5-layers-full/layered-react-loader.js b/packages/enhanced/test/configCases/import-false/5-layers-full/layered-react-loader.js new file mode 100644 index 00000000000..da3314523b7 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-layers-full/layered-react-loader.js @@ -0,0 +1,3 @@ +module.exports = function (source) { + return source.replace('__PLACEHOLDER__', 'This is layered react'); +}; diff --git a/packages/enhanced/test/configCases/import-false/5-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/import-false/5-layers-full/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-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/import-false/5-layers-full/package.json b/packages/enhanced/test/configCases/import-false/5-layers-full/package.json new file mode 100644 index 00000000000..4e44b5b102f --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-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/module-federation-with-shareScope/test.config.js b/packages/enhanced/test/configCases/import-false/5-layers-full/test.config.js similarity index 100% rename from packages/enhanced/test/configCases/container/module-federation-with-shareScope/test.config.js rename to packages/enhanced/test/configCases/import-false/5-layers-full/test.config.js diff --git a/packages/enhanced/test/configCases/import-false/5-layers-full/webpack.config.js b/packages/enhanced/test/configCases/import-false/5-layers-full/webpack.config.js new file mode 100644 index 00000000000..abb373e484d --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/5-layers-full/webpack.config.js @@ -0,0 +1,47 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +module.exports = { + entry: './index.js', + mode: 'development', + devtool: false, + target: 'node', + experiments: { + layers: true, + }, + output: { + filename: '[name].js', + uniqueName: '5-layers-full', + }, + module: { + rules: [ + { + test: /\.js$/, + layer: 'react-layer', + }, + { + test: /react\.js$/, + issuerLayer: 'react-layer', + loader: require.resolve('./layered-react-loader'), + }, + ], + }, + plugins: [ + // NEVER ADD shareScope to the plugin + new ModuleFederationPlugin({ + name: 'container_5', + filename: 'container.js', + library: { type: 'commonjs-module' }, + exposes: { + './ComponentA': './ComponentA', + }, + shared: { + react: { + singleton: true, + requiredVersion: false, + // import: false, + layer: 'react-layer', + }, + }, + }), + ], +}; diff --git a/packages/enhanced/test/configCases/import-false/6-layers-full/App.js b/packages/enhanced/test/configCases/import-false/6-layers-full/App.js new file mode 100644 index 00000000000..8d0bddb048e --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/6-layers-full/App.js @@ -0,0 +1,8 @@ +import ComponentA from 'containerA/ComponentA'; +import React from 'react'; +import LocalComponentA from './ComponentA'; + +export default function App() { + return `App rendered with React version: [${React()}]\nand remote component: [${ComponentA()}]\n and local component: [${LocalComponentA()}]`; +} + diff --git a/packages/enhanced/test/configCases/import-false/6-layers-full/ComponentA.js b/packages/enhanced/test/configCases/import-false/6-layers-full/ComponentA.js new file mode 100644 index 00000000000..80cdc08fcb1 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/6-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React, {layeredComponentsReact} from 'react'; + +export default function ComponentA() { + return `ComponentA with React: ${React()} layered with ${layeredComponentsReact()}`; +} diff --git a/packages/enhanced/test/configCases/import-false/6-layers-full/index.js b/packages/enhanced/test/configCases/import-false/6-layers-full/index.js new file mode 100644 index 00000000000..7eb4ec84000 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/6-layers-full/index.js @@ -0,0 +1,6 @@ +it('should load App with React and remote component', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe('App rendered with React version: [This is react 0.1.2]\nand remote component: [ComponentA rendered with React version: [This is react 0.1.2] with layer [This is layered react]]\n and local component: [ComponentA with React: This is react 0.1.2 layered with This is layered react]'); + }); +}); diff --git a/packages/enhanced/test/configCases/import-false/6-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/import-false/6-layers-full/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/6-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/module-federation-with-shareScope/package.json b/packages/enhanced/test/configCases/import-false/6-layers-full/package.json similarity index 100% rename from packages/enhanced/test/configCases/container/module-federation-with-shareScope/package.json rename to packages/enhanced/test/configCases/import-false/6-layers-full/package.json diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/test.config.js b/packages/enhanced/test/configCases/import-false/6-layers-full/test.config.js similarity index 100% rename from packages/enhanced/test/configCases/container/multiple-runtime-chunk/test.config.js rename to packages/enhanced/test/configCases/import-false/6-layers-full/test.config.js diff --git a/packages/enhanced/test/configCases/import-false/6-layers-full/webpack.config.js b/packages/enhanced/test/configCases/import-false/6-layers-full/webpack.config.js new file mode 100644 index 00000000000..bbf8c2f46c0 --- /dev/null +++ b/packages/enhanced/test/configCases/import-false/6-layers-full/webpack.config.js @@ -0,0 +1,44 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +module.exports = { + entry: './index.js', + mode: 'development', + target: 'node', + devtool: false, + experiments: { + layers: true, + }, + output: { + filename: '[name].js', + uniqueName: '6-layers-full', + }, + module: { + rules: [ + { + test: /\.js$/, + layer: 'react-layer', + }, + ], + }, + plugins: [ + // NEVER ADD shareScope to the plugin + new ModuleFederationPlugin({ + name: 'container_6', + filename: 'container.js', + library: { type: 'commonjs-module' }, + remotes: { + containerA: { + external: '../5-layers-full/container.js', + }, + }, + shared: { + react: { + singleton: true, + requiredVersion: false, + import: false, + layer: 'react-layer', + }, + }, + }), + ], +}; diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/App.js b/packages/enhanced/test/configCases/layers/7-layers-full/App.js new file mode 100644 index 00000000000..2169084c924 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/App.js @@ -0,0 +1,5 @@ +import React, { layeredComponentsReact } from 'react'; +import ComponentA from './ComponentA'; +export default function App() { + return `App (no layer) rendered with React version: [${React()}] with non-layered React value: [${layeredComponentsReact()}] and imported: ${ComponentA()}`; +} diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/ComponentA.js b/packages/enhanced/test/configCases/layers/7-layers-full/ComponentA.js new file mode 100644 index 00000000000..a621da4a98c --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React, { layeredComponentsReact } from 'react'; + +export default function ComponentA() { + return `ComponentA (in react-layer) rendered with React version: [${React()}] with layered React value: [${layeredComponentsReact()}]`; +} diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/emptyComponent.js b/packages/enhanced/test/configCases/layers/7-layers-full/emptyComponent.js new file mode 100644 index 00000000000..d4e3ba505f0 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/emptyComponent.js @@ -0,0 +1 @@ +export default 'testnoop'; diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/index.js b/packages/enhanced/test/configCases/layers/7-layers-full/index.js new file mode 100644 index 00000000000..1ca1567f452 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/index.js @@ -0,0 +1,30 @@ +if (typeof expect === 'undefined') { + global.expect = function (actual) { + return { + toContain: function (expected) { + if (!actual.includes(expected)) { + throw new Error(`Expected "${actual}" to contain "${expected}"`); + } + }, + toBe: function (expected) { + if (actual !== expected) { + throw new Error(`Expected "${actual}" to be "${expected}"`); + } + }, + }; + }; +} + +if (typeof it === 'undefined') { + global.it = function (name, fn) { + return fn(); + }; +} +it('should load App with React', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App (no layer) rendered with React version: [This is react 0.1.2] with non-layered React value: [No Layer] and imported: ComponentA (in react-layer) rendered with React version: [This is react 0.1.2] with layered React value: [react-layer]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/layered-react-loader.js b/packages/enhanced/test/configCases/layers/7-layers-full/layered-react-loader.js new file mode 100644 index 00000000000..aa306995e4f --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/layered-react-loader.js @@ -0,0 +1,4 @@ +module.exports = function(source) { + const issuerLayer = this._module?.layer; + return source.replace('No Layer', `${issuerLayer}`); +}; diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/layers/7-layers-full/node_modules/react.js new file mode 100644 index 00000000000..39f158129c9 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-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 = () => "No Layer"; diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/package.json b/packages/enhanced/test/configCases/layers/7-layers-full/package.json new file mode 100644 index 00000000000..4e44b5b102f --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-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/layers/7-layers-full/test.config.js b/packages/enhanced/test/configCases/layers/7-layers-full/test.config.js new file mode 100644 index 00000000000..1ca0b7cf737 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/test.config.js @@ -0,0 +1,6 @@ +module.exports = { + findBundle: function (i, options) { + return './main.js'; + return i === 0 ? './main.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/layers/7-layers-full/webpack.config.js b/packages/enhanced/test/configCases/layers/7-layers-full/webpack.config.js new file mode 100644 index 00000000000..b5c3f0e12df --- /dev/null +++ b/packages/enhanced/test/configCases/layers/7-layers-full/webpack.config.js @@ -0,0 +1,62 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); +const path = require('path'); + +module.exports = { + entry: './index.js', + mode: 'development', + devtool: false, + target: 'node', + experiments: { + layers: true, + }, + output: { + filename: '[name].js', + uniqueName: '7-layers-full', + }, + module: { + rules: [ + { + test: /ComponentA\.js$/, + layer: 'react-layer', + }, + { + test: /react\.js$/, + issuerLayer: 'react-layer', + layer: 'react-layer', + use: [ + { + loader: path.resolve(__dirname, './layered-react-loader.js'), + }, + ], + }, + ], + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'container_7', + filename: 'container.js', + library: { type: 'commonjs-module' }, + exposes: { + './ComponentA': './ComponentA', + './App': './App', + './noop': './emptyComponent', + }, + shared: { + react: { + singleton: true, + requiredVersion: false, + }, + randomvalue: { + request: 'react', + import: 'react', + shareKey: 'react', + shareScope: 'react-layer', + singleton: true, + requiredVersion: false, + layer: 'react-layer', + issuerLayer: 'react-layer', // only used by the compiler + }, + }, + }), + ], +}; diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/App.js b/packages/enhanced/test/configCases/layers/8-layers-full/App.js new file mode 100644 index 00000000000..b87db611351 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/App.js @@ -0,0 +1,11 @@ +import React, { layeredComponentsReact } from 'react'; +import ComponentA from 'containerA/ComponentA'; +import RemoteApp from 'containerA/App'; +import LocalComponentA from './ComponentA'; + +export default function App() { + return `App (no layer) rendered with React version: [${React()}] with non-layered React value: [${layeredComponentsReact()}] +Local Component: ${LocalComponentA()} +Remote Component from container7: ${ComponentA()} +Remote App from container7: ${RemoteApp()}`; +} diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/ComponentA.js b/packages/enhanced/test/configCases/layers/8-layers-full/ComponentA.js new file mode 100644 index 00000000000..6e1accf7b1a --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React, { layeredComponentsReact } from 'react'; + +export default function ComponentA() { + return `LocalComponentA (in react-layer) rendered with React version: [${React()}], layered React value: [${layeredComponentsReact()}]`; +} diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/index.js b/packages/enhanced/test/configCases/layers/8-layers-full/index.js new file mode 100644 index 00000000000..f76f33cb106 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/index.js @@ -0,0 +1,36 @@ +if (typeof expect === 'undefined') { + global.expect = function (actual) { + return { + toContain: function (expected) { + if (!actual.includes(expected)) { + throw new Error(`Expected "${actual}" to contain "${expected}"`); + } + }, + toBe: function (expected) { + if (actual !== expected) { + throw new Error(`Expected "${actual}" to be "${expected}"`); + } + }, + }; + }; +} + +if (typeof it === 'undefined') { + global.it = async function (name, fn) { + return await fn(); + }; +} + +it('should load App with React and both types of remote components', () => { + // load container first so share exists + return import('containerA/noop').then((m) => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered) + .toBe(`App (no layer) rendered with React version: [This is react 0.1.2] with non-layered React value: [No Layer] +Local Component: LocalComponentA (in react-layer) rendered with React version: [This is react 0.1.2], layered React value: [react-layer] +Remote Component from container7: ComponentA (in react-layer) rendered with React version: [This is react 0.1.2] with layered React value: [react-layer] +Remote App from container7: App (no layer) rendered with React version: [This is react 0.1.2] with non-layered React value: [No Layer] and imported: ComponentA (in react-layer) rendered with React version: [This is react 0.1.2] with layered React value: [react-layer]`); + }); + }); +}); diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/layerImport.js b/packages/enhanced/test/configCases/layers/8-layers-full/layerImport.js new file mode 100644 index 00000000000..361f3ac025d --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/layerImport.js @@ -0,0 +1,3 @@ +import LocalComponentA from './ComponentA'; + +export default LocalComponentA; diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/node_modules/package.json b/packages/enhanced/test/configCases/layers/8-layers-full/node_modules/package.json new file mode 100644 index 00000000000..87032da008a --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/node_modules/package.json @@ -0,0 +1,3 @@ +{ + "version": "2.1.0" +} diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/layers/8-layers-full/node_modules/react.js new file mode 100644 index 00000000000..17f75306c17 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/node_modules/react.js @@ -0,0 +1,4 @@ +let version = "2.1.0"; +export function setVersion(v) { version = v; } +export const layeredComponentsReact = () => "FEDERATION IS BROKEN, THIS VERION SHOULD NOT BE LOADED"; +export default () => `${version}`; diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/package.json b/packages/enhanced/test/configCases/layers/8-layers-full/package.json new file mode 100644 index 00000000000..9db5aa360a3 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "engines": { + "node": ">=10.13.0" + }, + "scripts": { + "build": "webpack --config=webpack.config.js" + }, + "dependencies": { + "react": "*" + } +} diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/test.config.js b/packages/enhanced/test/configCases/layers/8-layers-full/test.config.js new file mode 100644 index 00000000000..1ca0b7cf737 --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/test.config.js @@ -0,0 +1,6 @@ +module.exports = { + findBundle: function (i, options) { + return './main.js'; + return i === 0 ? './main.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/layers/8-layers-full/webpack.config.js b/packages/enhanced/test/configCases/layers/8-layers-full/webpack.config.js new file mode 100644 index 00000000000..df06b527f5a --- /dev/null +++ b/packages/enhanced/test/configCases/layers/8-layers-full/webpack.config.js @@ -0,0 +1,57 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +module.exports = { + entry: './index.js', + mode: 'development', + target: 'node', + devtool: false, + experiments: { + layers: true, + }, + output: { + filename: '[name].js', + uniqueName: '8-layers-full', + }, + module: { + rules: [ + { + layer: 'react-layer', + test: /ComponentA\.js$/, // Our local App will not be in a layer + }, + { + test: /react\.js$/, + issuerLayer: 'react-layer', + layer: 'react-layer', + }, + ], + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'container_8', + filename: 'container.js', + library: { type: 'commonjs-module' }, + remotes: { + containerA: { + external: '../7-layers-full/container.js', + }, + }, + shared: { + react: { + singleton: true, + requiredVersion: false, + import: false, + }, + randomvalue: { + request: 'react', + import: false, + shareKey: 'react', + singleton: true, + shareScope: 'react-layer', + requiredVersion: false, + layer: 'react-layer', + issuerLayer: 'react-layer', + }, + }, + }), + ], +}; diff --git a/packages/enhanced/test/configCases/sharing/3-layers-full/App.js b/packages/enhanced/test/configCases/sharing/3-layers-full/App.js new file mode 100644 index 00000000000..3a22bcfa277 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/3-layers-full/ComponentA.js b/packages/enhanced/test/configCases/sharing/3-layers-full/ComponentA.js new file mode 100644 index 00000000000..0e5b6e1ed71 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/3-layers-full/ComponentALayers.js b/packages/enhanced/test/configCases/sharing/3-layers-full/ComponentALayers.js new file mode 100644 index 00000000000..06bb446b99b --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/3-layers-full/ComponentALayers.js @@ -0,0 +1,7 @@ +import * as React from 'react'; + + +export default () => { + const result = `ComponentALayers rendered with [${React.layeredComponentsReact()}]`; + return result; +}; diff --git a/packages/enhanced/test/configCases/sharing/3-layers-full/index.js b/packages/enhanced/test/configCases/sharing/3-layers-full/index.js new file mode 100644 index 00000000000..d6027a53d04 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/3-layers-full/index.js @@ -0,0 +1,8 @@ +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]]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/sharing/3-layers-full/layered-react-loader.js b/packages/enhanced/test/configCases/sharing/3-layers-full/layered-react-loader.js new file mode 100644 index 00000000000..da3314523b7 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/3-layers-full/layered-react-loader.js @@ -0,0 +1,3 @@ +module.exports = function (source) { + return source.replace('__PLACEHOLDER__', 'This is layered react'); +}; diff --git a/packages/enhanced/test/configCases/sharing/3-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/sharing/3-layers-full/node_modules/react.js new file mode 100644 index 00000000000..6e63243a6eb --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/3-layers-full/package.json b/packages/enhanced/test/configCases/sharing/3-layers-full/package.json new file mode 100644 index 00000000000..4e44b5b102f --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/3-layers-full/test.config.js b/packages/enhanced/test/configCases/sharing/3-layers-full/test.config.js new file mode 100644 index 00000000000..861157bc4ed --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/3-layers-full/webpack.config.js b/packages/enhanced/test/configCases/sharing/3-layers-full/webpack.config.js new file mode 100644 index 00000000000..8ac09aaad48 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/3-layers-full/webpack.config.js @@ -0,0 +1,105 @@ +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: '1.2.3', + requiredVersion: '^1.0.0', + 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\.js$/, + include: /node_modules/, + 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/sharing/4-layers-full/App.js b/packages/enhanced/test/configCases/sharing/4-layers-full/App.js new file mode 100644 index 00000000000..337a111588d --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/App.js @@ -0,0 +1,12 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +import ComponentALayers from 'containerA/ComponentALayers'; +// import ComponentB from 'containerB/ComponentB'; +import ComponentC from './ComponentC'; +import LocalComponentB from './ComponentB'; +import LocalComponentALayers from './ComponentALayers'; +const ComponentB = LocalComponentB; +export default () => { + return `App rendered with [${React()}] and [${ComponentA()}] and [${ComponentALayers()}] and [${ComponentB()}] and [${ComponentC()}] and [${LocalComponentB()}] and [${LocalComponentALayers()}]`; +}; + diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentA.js b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentA.js new file mode 100644 index 00000000000..6061e699763 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function ComponentA() { + return `ComponentA (Regular React: ${React()})`; +} diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentALayers.js b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentALayers.js new file mode 100644 index 00000000000..e6ac2ad64ba --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentALayers.js @@ -0,0 +1,11 @@ +import * as React from 'react'; + +console.log('React import in ComponentALayers:', React); + +export default () => { + debugger; + console.log('Calling layeredComponentsReact:', React.layeredComponentsReact); + const result = `ComponentALayers rendered with [${React.layeredComponentsReact()}]`; + console.log('ComponentALayers result:', result); + return result; +}; diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentB.js b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentB.js new file mode 100644 index 00000000000..5865b7773af --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentB.js @@ -0,0 +1,7 @@ +import React, {from} from 'react'; + +export default () => { + debugger; + + return `ComponentB rendered with [${React()}] from ${from}`; +}; diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentC.js b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentC.js new file mode 100644 index 00000000000..d656172db54 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/ComponentC.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +// import ComponentB from 'containerB/ComponentB'; +const ComponentB = ComponentA +export default () => { + return `ComponentC rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/index.js b/packages/enhanced/test/configCases/sharing/4-layers-full/index.js new file mode 100644 index 00000000000..69adb7ac658 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/index.js @@ -0,0 +1,8 @@ +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] from 4-layers]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/node_modules/package.json b/packages/enhanced/test/configCases/sharing/4-layers-full/node_modules/package.json new file mode 100644 index 00000000000..87032da008a --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/node_modules/package.json @@ -0,0 +1,3 @@ +{ + "version": "2.1.0" +} diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/node_modules/react.js b/packages/enhanced/test/configCases/sharing/4-layers-full/node_modules/react.js new file mode 100644 index 00000000000..565230b8424 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/node_modules/react.js @@ -0,0 +1,4 @@ +let version = "2.1.0"; +export const from = '4-layers' +export function setVersion(v) { version = v; } +export default () => `This is react ${version}`; diff --git a/packages/enhanced/test/configCases/sharing/4-layers-full/package.json b/packages/enhanced/test/configCases/sharing/4-layers-full/package.json new file mode 100644 index 00000000000..be6238fec84 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/4-layers-full/test.config.js b/packages/enhanced/test/configCases/sharing/4-layers-full/test.config.js new file mode 100644 index 00000000000..861157bc4ed --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/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/sharing/4-layers-full/webpack.config.js b/packages/enhanced/test/configCases/sharing/4-layers-full/webpack.config.js new file mode 100644 index 00000000000..4d93a9314e8 --- /dev/null +++ b/packages/enhanced/test/configCases/sharing/4-layers-full/webpack.config.js @@ -0,0 +1,94 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); +const path = require('path') + +const common = { + name: 'layers_container_2', + exposes: { + './ComponentB': './ComponentB', + './ComponentC': './ComponentC', + }, + shared: { + react: { + version: false, + requiredVersion: false, + singleton: true, + }, + 'layered-react': { + request: 'react', + import: 'react', + shareKey: 'react', + version: '0', + requiredVersion: '^1.0.0', + 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', + } + ], + }, +}; + +module.exports = [ + { + ...commonConfig, + output: { + filename: '[name].js', + uniqueName: '4-layers-full', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'commonjs-module' }, + filename: 'container.js', + remotes: { + containerA: { + external:'../3-layers-full/container.js', + shareScope: 'layered-components', + }, + // containerB: './container.js', + }, + ...common, + }), + ], + }, + { + ...commonConfig, + experiments: { + ...commonConfig.experiments, + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: '4-layers-full-mjs', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: { + external: '../../3-layers-full/module/container.mjs', + shareScope: 'layered-components', + }, + // containerB: './container.mjs', + }, + ...common, + }), + ], + target: 'node14', + }, +]; diff --git a/packages/runtime/__tests__/share.ts b/packages/runtime/__tests__/share.ts index df07b6bdf91..09f47fb7448 100644 --- a/packages/runtime/__tests__/share.ts +++ b/packages/runtime/__tests__/share.ts @@ -154,3 +154,210 @@ export const shareInfoWithoutLibAndGetProvider = { }, }, }; + +export const layeredShareInfo1 = { + name: '@federation/layered-shared1', + remotes: [], + shared: { + react: { + version: '16.0.0', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.0', + from: '@federation/layered-shared1', + layer: 'base', + })), + }, + }, +}; + +export const layeredShareInfo2 = { + name: '@federation/layered-shared2', + remotes: [], + shared: { + react: { + version: '16.0.1', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.1', + from: '@federation/layered-shared2', + layer: 'base', + })), + }, + }, +}; + +export const layeredArrayShared = { + name: '@federation/layered-array-shared', + remotes: [], + shared: { + react: [ + { + version: '16.0.0', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.0', + from: '@federation/layered-array-shared', + })), + }, + { + version: '16.0.1', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.1', + from: '@federation/layered-array-shared', + layer: 'base', + })), + }, + { + version: '16.0.2', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.2', + from: '@federation/layered-array-shared', + layer: 'base', + })), + }, + ], + }, +}; + +export const nonLayeredShareInfo = { + name: '@federation/non-layered-shared', + remotes: [], + shared: { + react: { + version: '16.0.2', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.2', + from: '@federation/non-layered-shared', + })), + }, + }, +}; + +export const layeredSemverShared = { + name: '@federation/layered-semver-shared', + remotes: [], + shared: { + react: [ + { + // Base layer, v16 + version: '16.0.0', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '16.0.0', + from: '@federation/layered-semver-shared', + layer: 'base', + })), + }, + { + // Base layer, v17 + version: '17.0.0', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^17.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '17.0.0', + from: '@federation/layered-semver-shared', + layer: 'base', + })), + }, + { + // Base layer, v17 patch + version: '17.0.1', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^17.0.0', + eager: false, + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '17.0.1', + from: '@federation/layered-semver-shared', + layer: 'base', + })), + }, + { + // Non-layered version + version: '18.0.0', + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^18.0.0', + eager: false, + }, + get: () => + Promise.resolve(() => ({ + default: 'react', + version: '18.0.0', + from: '@federation/layered-semver-shared', + })), + }, + ], + }, +}; diff --git a/packages/runtime/__tests__/shares-layer.spec.ts b/packages/runtime/__tests__/shares-layer.spec.ts new file mode 100644 index 00000000000..17b140e56a7 --- /dev/null +++ b/packages/runtime/__tests__/shares-layer.spec.ts @@ -0,0 +1,1733 @@ +import { describe, it, expect, assert } from 'vitest'; +import { init } from '../src/index'; +import { ShareStrategy } from '../src/type'; +import { FederationHost } from '../src/core'; +import { ShareScopeMap, UserOptions } from '../src/type'; + +describe('layered shared with FederationHost', () => { + beforeEach(() => { + __FEDERATION__.__SHARE__ = {}; + }); + + it('should load layered share with version matching', async () => { + const vmConfig1 = { + name: '@shared-single/runtime-deps', + remotes: [], + shared: { + 'runtime-react': { + version: '16.0.0', + lib: () => { + return { from: '@shared-single/runtime-deps', layer: undefined }; + }, + }, + }, + }; + + const vmConfig2 = { + name: '@shared-single/runtime-deps2', + remotes: [], + shared: { + 'runtime-react': { + version: '17.0.2', + shareConfig: { + requiredVersion: '^17.0.0', + singleton: true, + layer: 'base', + }, + get: async () => () => { + return { from: '@shared-single/runtime-deps2', layer: 'base' }; + }, + }, + }, + }; + + const vmConfig3 = { + name: '@shared-single/runtime-deps3', + remotes: [], + shared: { + 'runtime-react': { + version: '18.0.0', + shareConfig: { + requiredVersion: '^18.0.0', + singleton: false, + layer: 'feature', + }, + lib: () => { + return { from: '@shared-single/runtime-deps3', layer: 'feature' }; + }, + }, + }, + }; + + const FM1 = new FederationHost(vmConfig1); + await FM1.loadShare<{ from: string; version: string; layer?: string }>( + 'runtime-react', + ); + const FM3 = new FederationHost(vmConfig3); + await FM3.loadShare<{ from: string; version: string; layer?: string }>( + 'runtime-react', + ); + + const FM2 = new FederationHost(vmConfig2); + const shared = await FM2.loadShare<{ + from: string; + version: string; + layer?: string; + }>('runtime-react', { + customShareInfo: { + shareConfig: { + requiredVersion: '^17.0.0', + singleton: true, + layer: 'base', + }, + }, + }); + assert(shared); + const sharedRes = shared(); + assert(sharedRes, "shared can't be null"); + expect(sharedRes.from).toEqual('@shared-single/runtime-deps2'); + expect(sharedRes.layer).toEqual('base'); + }); + + it('should handle eager layered shares', async () => { + const federationConfig1 = { + name: '@module-federation/eager-shared1', + remotes: [], + shared: { + 'eager-react': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: true, + layer: 'base', + }, + lib: () => ({ + name: 'eager-react-ins1', + version: '16.0.0', + layer: 'base', + }), + }, + }, + }; + + const federationConfig2 = { + name: '@module-federation/eager-shared2', + remotes: [], + shared: { + 'eager-react': { + version: '16.0.1', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: true, + layer: 'feature', + }, + lib: () => ({ + name: 'eager-react-ins2', + version: '16.0.1', + layer: 'feature', + }), + }, + }, + }; + + const FM = new FederationHost(federationConfig1); + const FM2 = new FederationHost(federationConfig2); + + const reactInstanceFactory = FM.loadShareSync<{ + version: string; + name: string; + layer: string; + }>('eager-react'); + const reactInstanceRes = reactInstanceFactory(); + assert(reactInstanceRes, "reactInstance can't be undefined"); + expect(reactInstanceRes.version).toBe('16.0.0'); + expect(reactInstanceRes.layer).toBe('base'); + + const reactInstance2 = FM2.loadShareSync<{ + version: string; + name: string; + layer: string; + }>('eager-react'); + const reactInstance2Res = reactInstance2(); + + assert(reactInstance2Res, "reactInstance can't be undefined"); + expect(reactInstance2Res.version).toBe('16.0.1'); + expect(reactInstance2Res.layer).toBe('feature'); + }); + + it('should handle strict version with layers', async () => { + const federationConfig1 = { + name: '@shared-single/runtime-deps', + remotes: [], + shared: { + 'runtime-react': { + version: '16.0.0', + lib: () => { + return { from: '@shared-single/runtime-deps', layer: 'base' }; + }, + }, + }, + }; + + const federationConfig2 = { + name: '@shared-single/runtime-deps2', + remotes: [], + shared: { + 'runtime-react': { + version: '17.0.2', + shareConfig: { + strictVersion: true, + singleton: true, + requiredVersion: '^17.0.0', + layer: 'base', + }, + lib: () => { + return { from: '@shared-single/runtime-deps2', layer: 'base' }; + }, + }, + }, + }; + + const FM1 = new FederationHost(federationConfig1); + const FM2 = new FederationHost(federationConfig2); + + await FM1.loadShare<{ from: string; version: string; layer: string }>( + 'runtime-react', + ); + FM2.initShareScopeMap('default', FM1.shareScopeMap['default']); + FM2.initShareScopeMap('(base)default', FM1.shareScopeMap['(base)default']); + + expect(function () { + FM2.loadShareSync<{ + version: string; + name: string; + layer: string; + }>('runtime-react'); + }).toThrowError('[ Federation Runtime ]: Version'); + }); + + it('should handle multiple layers with scope', async () => { + const existedShareScopeMap: ShareScopeMap = { + default: { + 'runtime-react': { + '16.0.1': { + version: '16.0.1', + get: () => () => { + return { from: '@shared-single/runtime-deps3', layer: undefined }; + }, + lib: () => { + return { from: '@shared-single/runtime-deps3', layer: undefined }; + }, + shareConfig: { + requiredVersion: false, + singleton: true, + eager: false, + strictVersion: false, + }, + scope: ['default'], + useIn: ['@shared-single/runtime-deps3'], + from: '@shared-single/runtime-deps3', + deps: [], + strategy: 'version-first', + }, + }, + }, + '(base)default': { + 'runtime-react': { + '16.0.2': { + version: '16.0.2', + get: () => () => { + return { from: '@shared-single/runtime-deps2', layer: 'base' }; + }, + lib: () => { + return { from: '@shared-single/runtime-deps2', layer: 'base' }; + }, + shareConfig: { + requiredVersion: false, + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: ['default'], + useIn: ['@shared-single/runtime-deps2'], + from: '@shared-single/runtime-deps2', + deps: [], + strategy: 'version-first', + }, + }, + }, + }; + + const federationConfig1: UserOptions = { + name: '@shared-single/runtime-deps', + remotes: [], + shared: { + 'runtime-react': { + version: '16.0.0', + scope: 'default', + shareConfig: { + layer: 'base', + requiredVersion: '^16.0.0', + }, + lib: () => { + return { from: '@shared-single/runtime-deps', layer: 'base' }; + }, + }, + }, + }; + + const FM1 = new FederationHost(federationConfig1); + FM1.initShareScopeMap('default', existedShareScopeMap['default']); + FM1.initShareScopeMap( + '(base)default', + existedShareScopeMap['(base)default'], + ); + + const shared = await FM1.loadShare<{ + from: string; + version: string; + layer?: string; + }>('runtime-react', { + customShareInfo: { + shareConfig: { + layer: 'base', + requiredVersion: '^16.0.0', + }, + }, + }); + assert(shared, "shared can't be null"); + + const sharedRes = shared(); + assert(sharedRes, "sharedRes can't be null"); + expect(sharedRes.from).toEqual('@shared-single/runtime-deps2'); + expect(sharedRes.layer).toEqual('base'); + }); +}); + +describe('layered shared', () => { + let federation: any; + + beforeEach(() => { + __FEDERATION__.__SHARE__ = {}; + }); + + it('should not create composite scope when no layers exist', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: { + version: '16.0.0', + lib: () => `mock library react at 16.0.0 from @federation/layer-test`, + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + }); + + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(1); + expect(federation.shareScopeMap['default'].react).toBeDefined(); + expect(federation.shareScopeMap['default'].react['16.0.0']).toBeDefined(); + }); + + it('should only register in composite scope when layer exists', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: { + version: '16.0.0', + lib: () => + `mock library react at 16.0.0 from @federation/layer-test with layer base`, + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + // Both scopes should exist + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(federation.shareScopeMap).toHaveProperty('(base)default'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(2); + + // Share should only be in layer scope + expect(federation.shareScopeMap['(base)default'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['16.0.0'], + ).toBeDefined(); + expect(federation.shareScopeMap['default'].react).toBeUndefined(); + }); + + it('should handle mixed layered and non-layered shares correctly', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: [ + { + version: '16.0.0', + lib: () => + `mock library react at 16.0.0 from @federation/layer-test`, + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + { + version: '17.0.0', + lib: () => + `mock library react at 17.0.0 from @federation/layer-test with layer base`, + shareConfig: { + singleton: true, + requiredVersion: '^17.0.0', + layer: 'base', + }, + }, + ], + }, + }); + + // Both scopes should exist + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(federation.shareScopeMap).toHaveProperty('(base)default'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(2); + + // Non-layered share should be in default scope + expect(federation.shareScopeMap['default'].react).toBeDefined(); + expect(federation.shareScopeMap['default'].react['16.0.0']).toBeDefined(); + expect(federation.shareScopeMap['default'].react['17.0.0']).toBeUndefined(); + + // Layered share should be in layer scope + expect(federation.shareScopeMap['(base)default'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['17.0.0'], + ).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['16.0.0'], + ).toBeUndefined(); + }); + + it('should respect explicit default scope with no layer', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: { + version: '16.0.0', + lib: () => ({ + name: 'react', + version: '16.0.0', + }), + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + }); + + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(1); + expect(federation.shareScopeMap['default'].react).toBeDefined(); + expect(federation.shareScopeMap['default'].react['16.0.0']).toBeDefined(); + }); + + it('should create composite scope when layer exists regardless of explicit scope', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: { + version: '16.0.0', + lib: () => ({ + name: 'react', + version: '16.0.0', + }), + scope: ['default'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + // Both scopes should exist + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(federation.shareScopeMap).toHaveProperty('(base)default'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(2); + + // Share should only be in layer scope + expect(federation.shareScopeMap['(base)default'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['16.0.0'], + ).toBeDefined(); + expect(federation.shareScopeMap['default'].react).toBeUndefined(); + }); + + it('should handle multiple scopes with layer', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: { + version: '16.0.0', + lib: () => ({ + name: 'react', + version: '16.0.0', + }), + scope: ['default', 'custom'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + // Should create composite scopes for all original scopes + expect(federation.shareScopeMap).toHaveProperty('(base)default'); + expect(federation.shareScopeMap).toHaveProperty('(base)custom'); + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(federation.shareScopeMap).toHaveProperty('custom'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(4); + + // Share should only be in layer scopes + expect(federation.shareScopeMap['(base)default'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['16.0.0'], + ).toBeDefined(); + expect(federation.shareScopeMap['(base)custom'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)custom'].react['16.0.0'], + ).toBeDefined(); + expect(federation.shareScopeMap['default'].react).toBeUndefined(); + expect(federation.shareScopeMap['custom'].react).toBeUndefined(); + }); + + it('should handle array shares with custom scopes and mixed layers', () => { + federation = init({ + name: '@federation/layer-test', + remotes: [], + shared: { + react: [ + { + version: '16.0.0', + lib: () => + `mock library react at 16.0.0 from @federation/layer-test`, + scope: ['default', 'custom'], + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + { + version: '17.0.0', + lib: () => + `mock library react at 17.0.0 from @federation/layer-test with layer base`, + scope: ['default', 'custom'], + shareConfig: { + singleton: true, + requiredVersion: '^17.0.0', + layer: 'base', + }, + }, + ], + }, + }); + + // Should create all scopes + expect(federation.shareScopeMap).toHaveProperty('default'); + expect(federation.shareScopeMap).toHaveProperty('custom'); + expect(federation.shareScopeMap).toHaveProperty('(base)default'); + expect(federation.shareScopeMap).toHaveProperty('(base)custom'); + expect(Object.keys(federation.shareScopeMap)).toHaveLength(4); + + // Non-layered share should be in original scopes + expect(federation.shareScopeMap['default'].react).toBeDefined(); + expect(federation.shareScopeMap['default'].react['16.0.0']).toBeDefined(); + expect(federation.shareScopeMap['default'].react['17.0.0']).toBeUndefined(); + expect(federation.shareScopeMap['custom'].react).toBeDefined(); + expect(federation.shareScopeMap['custom'].react['16.0.0']).toBeDefined(); + expect(federation.shareScopeMap['custom'].react['17.0.0']).toBeUndefined(); + + // Layered share should only be in composite scopes + expect(federation.shareScopeMap['(base)default'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['17.0.0'], + ).toBeDefined(); + expect( + federation.shareScopeMap['(base)default'].react['16.0.0'], + ).toBeUndefined(); + expect(federation.shareScopeMap['(base)custom'].react).toBeDefined(); + expect( + federation.shareScopeMap['(base)custom'].react['17.0.0'], + ).toBeDefined(); + expect( + federation.shareScopeMap['(base)custom'].react['16.0.0'], + ).toBeUndefined(); + }); +}); +describe('layered share loading', () => { + beforeEach(() => { + __FEDERATION__.__SHARE__ = {}; + }); + + it('should load share from base layer when layer is specified in shareConfig', async () => { + const provider = init({ + name: '@federation/shared-config-provider', + remotes: [], + shared: { + 'react-dom': { + version: '16.0.0', + lib: () => ({ + version: '16.0.0', + from: '@federation/shared-config-provider', + layer: 'base', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + const consumer = init({ + name: '@federation/shared-config-consumer', + remotes: [], + shared: { + 'react-dom': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + // Initialize both default and layer scopes + consumer.initShareScopeMap('default', provider.shareScopeMap['default']); + consumer.initShareScopeMap( + '(base)default', + provider.shareScopeMap['(base)default'], + ); + + const reactDomInstance = await consumer.loadShare<{ + version: string; + from: string; + layer: string; + }>('react-dom', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }); + + assert(reactDomInstance); + const reactDomRes = reactDomInstance(); + assert(reactDomRes, "reactDom can't be undefined"); + expect(reactDomRes.from).toBe('@federation/shared-config-provider'); + expect(reactDomRes.version).toBe('16.0.0'); + expect(reactDomRes.layer).toBe('base'); + }); + + it('should load shares from different layers based on layer specified in shareConfig', async () => { + const provider = init({ + name: '@federation/shared-config-provider', + remotes: [], + shared: { + 'react-dom': [ + { + version: '16.0.0', + lib: () => ({ + version: '16.0.0', + from: '@federation/shared-config-provider', + layer: 'base', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + { + version: '16.0.1', + lib: () => ({ + version: '16.0.1', + from: '@federation/shared-config-provider', + layer: 'feature', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + ], + }, + }); + + const consumerBase = init({ + name: '@federation/shared-config-consumer-base', + remotes: [], + shared: { + 'react-dom': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + const consumerFeature = init({ + name: '@federation/shared-config-consumer-feature', + remotes: [], + shared: { + 'react-dom': { + version: '16.0.1', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + }, + }); + + // Initialize both default and layer scopes for base consumer + consumerBase.initShareScopeMap( + 'default', + provider.shareScopeMap['default'], + ); + consumerBase.initShareScopeMap( + '(base)default', + provider.shareScopeMap['(base)default'], + ); + + // Initialize both default and layer scopes for feature consumer + consumerFeature.initShareScopeMap( + 'default', + provider.shareScopeMap['default'], + ); + consumerFeature.initShareScopeMap( + '(feature)default', + provider.shareScopeMap['(feature)default'], + ); + + // Load from base layer + const baseReactDom = await consumerBase.loadShare<{ + version: string; + from: string; + layer: string; + }>('react-dom', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }); + + assert(baseReactDom); + const baseReactDomRes = baseReactDom(); + assert(baseReactDomRes, "baseReactDom can't be undefined"); + expect(baseReactDomRes.from).toBe('@federation/shared-config-provider'); + expect(baseReactDomRes.version).toBe('16.0.0'); + expect(baseReactDomRes.layer).toBe('base'); + + // Load from feature layer + const featureReactDom = await consumerFeature.loadShare<{ + version: string; + from: string; + layer: string; + }>('react-dom', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + }); + + assert(featureReactDom); + const featureReactDomRes = featureReactDom(); + assert(featureReactDomRes, "featureReactDom can't be undefined"); + expect(featureReactDomRes.from).toBe('@federation/shared-config-provider'); + expect(featureReactDomRes.version).toBe('16.0.1'); + expect(featureReactDomRes.layer).toBe('feature'); + }); + + it('should merge layered shares from multiple instances', () => { + const baseInstance = init({ + name: '@federation/base', + remotes: [], + shared: { + 'react-dom': { + version: '16.0.0', + lib: () => ({ + version: '16.0.0', + from: '@federation/base', + layer: 'base', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + // Initialize the feature instance with the same name to ensure merging + const featureInstance = init({ + name: '@federation/base', + remotes: [], + shared: { + 'react-dom': { + version: '16.0.1', + lib: () => ({ + version: '16.0.1', + from: '@federation/feature', + layer: 'feature', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + }, + }); + + expect(baseInstance).toBe(featureInstance); + expect(baseInstance.shareScopeMap).toHaveProperty('(base)default'); + expect(baseInstance.shareScopeMap).toHaveProperty('(feature)default'); + }); + + it('should handle singleton shares across layers', async () => { + const provider = init({ + name: '@federation/singleton-provider', + remotes: [], + shared: { + 'singleton-react': [ + { + version: '16.0.0', + get: () => + new Promise< + () => { version: string; from: string; layer: string } + >((resolve) => { + setTimeout(() => { + resolve(() => ({ + version: '16.0.0', + from: '@federation/singleton-provider', + layer: 'base', + })); + }, 500); + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + { + version: '16.0.1', + get: () => + new Promise< + () => { version: string; from: string; layer: string } + >((resolve) => { + setTimeout(() => { + resolve(() => ({ + version: '16.0.1', + from: '@federation/singleton-provider', + layer: 'feature', + })); + }, 500); + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + ], + }, + }); + + const consumer = init({ + name: '@federation/singleton-consumer', + remotes: [], + shared: { + 'singleton-react': [ + { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + { + version: '16.0.1', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + ], + }, + }); + + // Initialize both default and layer scopes + consumer.initShareScopeMap('default', provider.shareScopeMap['default']); + consumer.initShareScopeMap( + '(base)default', + provider.shareScopeMap['(base)default'], + ); + consumer.initShareScopeMap( + '(feature)default', + provider.shareScopeMap['(feature)default'], + ); + + // Register shares in provider's scope maps + provider.shareScopeMap['(base)default'] = { + 'singleton-react': { + '16.0.0': { + ...provider.options.shared['singleton-react'][0], + loaded: true, + get: () => + Promise.resolve(() => ({ + version: '16.0.0', + from: '@federation/singleton-provider', + layer: 'base', + })), + }, + }, + }; + provider.shareScopeMap['(feature)default'] = { + 'singleton-react': { + '16.0.1': { + ...provider.options.shared['singleton-react'][1], + loaded: true, + get: () => + Promise.resolve(() => ({ + version: '16.0.1', + from: '@federation/singleton-provider', + layer: 'feature', + })), + }, + }, + }; + + // Initialize consumer's share scopes + consumer.shareScopeMap['(base)default'] = { + 'singleton-react': { + '16.0.0': { + ...provider.shareScopeMap['(base)default']['singleton-react'][ + '16.0.0' + ], + }, + }, + }; + consumer.shareScopeMap['(feature)default'] = { + 'singleton-react': { + '16.0.1': { + ...provider.shareScopeMap['(feature)default']['singleton-react'][ + '16.0.1' + ], + }, + }, + }; + + // Load from base layer + const baseReact = await consumer.loadShare<{ + version: string; + from: string; + layer: string; + }>('singleton-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }); + + assert(baseReact); + const baseReactRes = baseReact(); + assert(baseReactRes, "baseReact can't be undefined"); + expect(baseReactRes.from).toBe('@federation/singleton-provider'); + expect(baseReactRes.version).toBe('16.0.0'); + expect(baseReactRes.layer).toBe('base'); + + // Load from feature layer + const featureReact = await consumer.loadShare<{ + version: string; + from: string; + layer: string; + }>('singleton-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + }); + + assert(featureReact); + const featureReactRes = featureReact(); + assert(featureReactRes, "featureReact can't be undefined"); + expect(featureReactRes.from).toBe('@federation/singleton-provider'); + expect(featureReactRes.version).toBe('16.0.1'); + expect(featureReactRes.layer).toBe('feature'); + }); + + it('should cache layered shares independently', async () => { + let baseId = 0; + let featureId = 0; + const provider = init({ + name: '@federation/cache-provider', + remotes: [], + shared: { + 'cached-react': [ + { + version: '16.0.0', + get: () => + new Promise< + () => { + version: string; + from: string; + layer: string; + uniqueId: number; + } + >((resolve) => { + setTimeout(() => { + baseId++; + resolve(() => ({ + version: '16.0.0', + from: '@federation/cache-provider', + layer: 'base', + uniqueId: baseId, + })); + }, 500); + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + { + version: '16.0.1', + get: () => + new Promise< + () => { + version: string; + from: string; + layer: string; + uniqueId: number; + } + >((resolve) => { + setTimeout(() => { + featureId++; + resolve(() => ({ + version: '16.0.1', + from: '@federation/cache-provider', + layer: 'feature', + uniqueId: featureId, + })); + }, 500); + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + ], + }, + }); + + // Register shares in provider's scope maps + provider.shareScopeMap['(base)default'] = { + 'cached-react': { + '16.0.0': { + ...provider.options.shared['cached-react'][0], + loaded: true, + get: () => + Promise.resolve(() => ({ + version: '16.0.0', + from: '@federation/cache-provider', + layer: 'base', + uniqueId: 1, + })), + }, + }, + }; + provider.shareScopeMap['(feature)default'] = { + 'cached-react': { + '16.0.1': { + ...provider.options.shared['cached-react'][1], + loaded: true, + get: () => + Promise.resolve(() => ({ + version: '16.0.1', + from: '@federation/cache-provider', + layer: 'feature', + uniqueId: 1, + })), + }, + }, + }; + + // Load base layer shares in parallel + const [baseReact1, baseReact2] = await Promise.all([ + provider.loadShare<{ + version: string; + from: string; + layer: string; + uniqueId: number; + }>('cached-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }), + provider.loadShare<{ + version: string; + from: string; + layer: string; + uniqueId: number; + }>('cached-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }), + ]); + + assert(baseReact1); + assert(baseReact2); + const baseReact1Res = baseReact1(); + const baseReact2Res = baseReact2(); + assert(baseReact1Res, "baseReact1 can't be undefined"); + assert(baseReact2Res, "baseReact2 can't be undefined"); + expect(baseReact1Res.uniqueId).toBe(1); + expect(baseReact2Res.uniqueId).toBe(1); + expect(baseReact1Res).toStrictEqual(baseReact2Res); + + // Load feature layer shares in parallel + const [featureReact1, featureReact2] = await Promise.all([ + provider.loadShare<{ + version: string; + from: string; + layer: string; + uniqueId: number; + }>('cached-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + }), + provider.loadShare<{ + version: string; + from: string; + layer: string; + uniqueId: number; + }>('cached-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'feature', + }, + }, + }), + ]); + + assert(featureReact1); + assert(featureReact2); + const featureReact1Res = featureReact1(); + const featureReact2Res = featureReact2(); + assert(featureReact1Res, "featureReact1 can't be undefined"); + assert(featureReact2Res, "featureReact2 can't be undefined"); + expect(featureReact1Res.uniqueId).toBe(1); + expect(featureReact2Res.uniqueId).toBe(1); + expect(featureReact1Res).toStrictEqual(featureReact2Res); + + // Verify base and feature shares are different + expect(baseReact1Res).not.toStrictEqual(featureReact1Res); + expect(baseReact1Res.layer).toBe('base'); + expect(featureReact1Res.layer).toBe('feature'); + }); + + it('should inject runtime dependencies with layers', async () => { + const baseReact = () => ({ + from: '@federation/runtime-deps', + layer: 'base', + }); + + const featureReact = () => ({ + from: '@federation/runtime-deps', + layer: 'feature', + }); + + const provider = init({ + name: '@federation/runtime-deps', + remotes: [], + shared: {}, + }); + + provider.initOptions({ + name: '@federation/runtime-deps', + remotes: [], + shared: { + 'runtime-react': [ + { + version: '16.0.0', + lib: baseReact, + shareConfig: { + layer: 'base', + requiredVersion: '^16.0.0', + }, + }, + { + version: '16.0.1', + lib: featureReact, + shareConfig: { + layer: 'feature', + requiredVersion: '^16.0.0', + }, + }, + ], + }, + }); + + const consumer = init({ + name: '@federation/runtime-deps-consumer', + remotes: [], + shared: { + 'runtime-react': [ + { + version: '16.0.0', + shareConfig: { + requiredVersion: '^16.0.0', + singleton: false, + layer: 'base', + }, + }, + { + version: '16.0.1', + shareConfig: { + requiredVersion: '^16.0.0', + singleton: false, + layer: 'feature', + }, + }, + ], + }, + }); + + // Initialize both default and layer scopes + consumer.initShareScopeMap('default', provider.shareScopeMap['default']); + consumer.initShareScopeMap( + '(base)default', + provider.shareScopeMap['(base)default'], + ); + consumer.initShareScopeMap( + '(feature)default', + provider.shareScopeMap['(feature)default'], + ); + + // Load base layer + const baseShare = await consumer.loadShare<{ + from: string; + layer: string; + }>('runtime-react', { + customShareInfo: { + shareConfig: { + requiredVersion: '^16.0.0', + singleton: false, + layer: 'base', + }, + }, + }); + + assert(baseShare); + const baseShareRes = baseShare(); + assert(baseShareRes, "baseShare can't be null"); + expect(baseShareRes.from).toBe('@federation/runtime-deps'); + expect(baseShareRes.layer).toBe('base'); + + // Load feature layer + const featureShare = await consumer.loadShare<{ + from: string; + layer: string; + }>('runtime-react', { + customShareInfo: { + shareConfig: { + requiredVersion: '^16.0.0', + singleton: false, + layer: 'feature', + }, + }, + }); + + assert(featureShare); + const featureShareRes = featureShare(); + assert(featureShareRes, "featureShare can't be null"); + expect(featureShareRes.from).toBe('@federation/runtime-deps'); + expect(featureShareRes.layer).toBe('feature'); + }); + + it('should handle loading mixed layered and non-layered shares correctly', async () => { + const provider = init({ + name: '@federation/mixed-provider', + remotes: [], + shared: { + 'mixed-react': [ + // Non-layered version + { + version: '16.0.0', + lib: () => ({ + version: '16.0.0', + from: '@federation/mixed-provider', + layer: undefined, + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + // Layered version + { + version: '16.0.1', + lib: () => ({ + version: '16.0.1', + from: '@federation/mixed-provider', + layer: 'base', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + ], + }, + }); + + const consumer = init({ + name: '@federation/mixed-consumer', + remotes: [], + shared: { + 'mixed-react': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + }); + + // Initialize scopes + consumer.initShareScopeMap('default', provider.shareScopeMap['default']); + consumer.initShareScopeMap( + '(base)default', + provider.shareScopeMap['(base)default'], + ); + + // Set up both layered and non-layered shares in the scope map + consumer.shareScopeMap['default'] = { + 'mixed-react': { + '16.0.0': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + scope: ['default'], + useIn: [], + deps: [], + from: '@federation/layered-provider', + loaded: true, + strategy: 'version-first', + lib: () => ({ + version: '16.0.0', + from: '@federation/layered-provider', + layer: undefined, + }), + get: () => + Promise.resolve(() => ({ + version: '16.0.0', + from: '@federation/layered-provider', + layer: undefined, + })), + }, + }, + }; + + consumer.shareScopeMap['(base)default'] = { + 'mixed-react': { + '16.0.1': { + version: '16.0.1', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + scope: ['default'], + useIn: [], + deps: [], + from: '@federation/layered-provider', + loaded: true, + strategy: 'version-first', + lib: () => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + }), + get: () => + Promise.resolve(() => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + })), + }, + }, + }; + + // Test 1: Load layered version when both exist + const layeredShare = await consumer.loadShare<{ + version: string; + from: string; + layer: string | undefined; + }>('mixed-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }); + + assert(layeredShare); + const layeredRes = layeredShare(); + assert(layeredRes, "layeredShare can't be undefined"); + expect(layeredRes.from).toBe('@federation/layered-provider'); + expect(layeredRes.version).toBe('16.0.1'); + expect(layeredRes.layer).toBe('base'); + + // Test 2: Load non-layered version when both exist + const nonLayeredShare = await consumer.loadShare<{ + version: string; + from: string; + layer: string | undefined; + }>('mixed-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }); + + assert(nonLayeredShare); + const nonLayeredRes = nonLayeredShare(); + assert(nonLayeredRes, "nonLayeredShare can't be undefined"); + expect(nonLayeredRes.from).toBe('@federation/layered-provider'); + expect(nonLayeredRes.version).toBe('16.0.0'); + expect(nonLayeredRes.layer).toBeUndefined(); + + // Test 3: Initialize a provider with only non-layered version + const nonLayeredOnlyProvider = init({ + name: '@federation/non-layered-provider', + remotes: [], + shared: { + 'mixed-react': { + version: '16.0.0', + lib: () => ({ + version: '16.0.0', + from: '@federation/non-layered-provider', + layer: undefined, + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + }); + + const nonLayeredConsumer = init({ + name: '@federation/non-layered-consumer', + remotes: [], + shared: { + 'mixed-react': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + }); + + // Set up non-layered provider's scope map + nonLayeredOnlyProvider.shareScopeMap['default'] = { + 'mixed-react': { + '16.0.0': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + scope: ['default'], + useIn: [], + deps: [], + from: '@federation/non-layered-provider', + loaded: true, + strategy: 'version-first', + get: () => + Promise.resolve(() => ({ + version: '16.0.0', + from: '@federation/non-layered-provider', + layer: undefined, + })), + }, + }, + }; + + nonLayeredConsumer.initShareScopeMap( + 'default', + nonLayeredOnlyProvider.shareScopeMap['default'], + ); + nonLayeredConsumer.shareScopeMap['default'] = { + 'mixed-react': { + '16.0.0': { + ...nonLayeredOnlyProvider.shareScopeMap['default']['mixed-react'][ + '16.0.0' + ], + }, + }, + }; + + // Try to load with layer when only non-layered exists + const nonLayeredOnlyShare = await nonLayeredConsumer.loadShare<{ + version: string; + from: string; + layer: string | undefined; + }>('mixed-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }); + + assert(nonLayeredOnlyShare); + const nonLayeredOnlyRes = nonLayeredOnlyShare(); + assert(nonLayeredOnlyRes, "nonLayeredOnlyShare can't be undefined"); + expect(nonLayeredOnlyRes.from).toBe('@federation/non-layered-provider'); + expect(nonLayeredOnlyRes.version).toBe('16.0.0'); + expect(nonLayeredOnlyRes.layer).toBeUndefined(); + + // Test 4: Initialize a provider with only layered version + const layeredOnlyProvider = init({ + name: '@federation/layered-provider', + remotes: [], + shared: { + 'mixed-react': { + version: '16.0.1', + lib: () => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + }), + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + }, + }, + }); + + const layeredConsumer = init({ + name: '@federation/layered-consumer', + remotes: [], + shared: { + 'mixed-react': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + }); + + // Set up layered provider's scope map + layeredOnlyProvider.shareScopeMap['(base)default'] = { + 'mixed-react': { + '16.0.1': { + version: '16.0.1', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + scope: ['default'], + useIn: [], + deps: [], + from: '@federation/layered-provider', + loaded: true, + strategy: 'version-first', + get: () => + Promise.resolve(() => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + })), + }, + }, + }; + + layeredConsumer.initShareScopeMap( + 'default', + layeredOnlyProvider.shareScopeMap['default'], + ); + layeredConsumer.initShareScopeMap( + '(base)default', + layeredOnlyProvider.shareScopeMap['(base)default'], + ); + + // Set up both layered and non-layered shares in the scope map + layeredConsumer.shareScopeMap['default'] = { + 'mixed-react': { + '16.0.0': { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + scope: ['default'], + useIn: [], + deps: [], + from: '@federation/layered-provider', + loaded: true, + strategy: 'version-first', + lib: () => ({ + version: '16.0.0', + from: '@federation/layered-provider', + layer: undefined, + }), + get: () => + Promise.resolve(() => ({ + version: '16.0.0', + from: '@federation/layered-provider', + layer: undefined, + })), + }, + }, + }; + + layeredConsumer.shareScopeMap['(base)default'] = { + 'mixed-react': { + '16.0.1': { + version: '16.0.1', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + scope: ['default'], + useIn: [], + deps: [], + from: '@federation/layered-provider', + loaded: true, + strategy: 'version-first', + lib: () => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + }), + get: () => + Promise.resolve(() => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + })), + }, + }, + }; + + // Try to load with layer when only layered exists + const layeredOnlyShare = await layeredConsumer.loadShare<{ + version: string; + from: string; + layer: string | undefined; + }>('mixed-react', { + customShareInfo: { + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + layer: 'base', + }, + get: () => + Promise.resolve(() => ({ + version: '16.0.1', + from: '@federation/layered-provider', + layer: 'base', + })), + }, + }); + + assert(layeredOnlyShare); + const layeredOnlyRes = layeredOnlyShare(); + assert(layeredOnlyRes, "layeredOnlyShare can't be undefined"); + expect(layeredOnlyRes.from).toBe('@federation/layered-provider'); + expect(layeredOnlyRes.version).toBe('16.0.1'); + expect(layeredOnlyRes.layer).toBe('base'); + }); +}); diff --git a/packages/runtime/src/core.md b/packages/runtime/src/core.md deleted file mode 100644 index 63efa42eea5..00000000000 --- a/packages/runtime/src/core.md +++ /dev/null @@ -1,147 +0,0 @@ -# FederationHost Class - -## Overview -`FederationHost` orchestrates module federation, managing remote modules and shared dependencies. It utilizes a sophisticated plugin architecture and lifecycle hooks for comprehensive control and flexibility. - -## Constructor -```typescript -constructor(userOptions: UserOptions) -``` -Initializes `FederationHost` with user-defined options. - -### Parameters -- `userOptions: UserOptions`: Configuration for the FederationHost. - - **Properties**: - - `name`: `string` - Name of the host. - - `plugins`: `Array` - List of plugins. - - `remotes`: `Array` - List of remote modules. - - `shared`: `Record` - Configuration for shared modules. - - `inBrowser`: `boolean` - Flag to indicate if running in a browser environment. - -## Properties -- **options: Options** - - Configuration settings of FederationHost. - - **Properties**: - - `id`: `string` - Unique identifier for the host. - - `name`: `string` - Name of the host. - - `plugins`: `Array` - List of plugins. - - `remotes`: `Array` - List of remote modules. - - `shared`: `Record` - Configuration for shared modules. - - `inBrowser`: `boolean` - Flag to indicate if running in a browser environment. -- **hooks: PluginSystem** - - Lifecycle hooks for FederationHost interaction. -- **version: string** - - Version of FederationHost. -- **name: string** - - Name identifier for FederationHost. -- **moduleCache: Map** - - Cache for stored modules. -- **snapshotHandler: SnapshotHandler** - - Manages snapshots in federation process. -- **loaderHook: PluginSystem** - - Plugin system for module loading operations. - -## Methods - -### `initOptions` -```typescript -initOptions(userOptions: UserOptions): Options -``` -Initializes or updates FederationHost options. - -### `loadShare` -```typescript -async loadShare(pkgName: string, customShareInfo?: Partial): Promise T | undefined)> -``` -Loads a shared module asynchronously. - -### `loadShareSync` -```typescript -loadShareSync(pkgName: string): () => T | never -``` -Synchronously loads a shared module. - -### `loadRemote` -```typescript -async loadRemote(id: string, options?: { loadFactory?: boolean }): Promise -``` -Loads a remote module asynchronously. - -### `preloadRemote` -```typescript -async preloadRemote(preloadOptions: Array): Promise -``` -Preloads remote modules based on configurations. - -### `initializeSharing` -```typescript -initializeSharing(shareScopeName?: string): boolean | Promise -``` -Initializes sharing sequences for shared scopes. - -### `registerRemotes` -```typescript -registerRemotes(remotes: Remote[], options?: { force?: boolean }): void -``` -Register remotes after init. - -## Hooks -`FederationHost` offers various lifecycle hooks for interacting at different stages of the module federation process. These hooks include: - -- **`beforeInit`**: `SyncWaterfallHook<{ userOptions: UserOptions; options: Options; origin: FederationHost; shareInfo: ShareInfos; }>` - - Updates Federation Host configurations before the initialization process of remote containers. -- **`init`**: `SyncHook<[{ options: Options; origin: FederationHost; }], void>` - - Called during the initialization of remote containers. -- **`beforeRequest`**: `AsyncWaterfallHook<{ id: string; options: Options; origin: FederationHost; }>` - - Invoked before resolving a remote container, useful for injecting the container or updating something ahead of the lookup. -- **`afterResolve`**: `AsyncWaterfallHook` - - Called after resolving a container, allowing redirection or modification of resolved information. -- **`beforeInitContainer`**: `AsyncWaterfallHook<{shareScope: ShareScopeMap[string];initScope: InitScope;remoteEntryInitOptions: RemoteEntryInitOptions;origin: FederationHost;}>` - - Get the init parameters and use them before the remote container init method is called. -- **`initContainer`**: `AsyncWaterfallHook<{shareScope: ShareScopeMap[string];initScope: InitScope;remoteEntryInitOptions: RemoteEntryInitOptions;remoteEntryExports: RemoteEntryExports;origin: FederationHost;}>` - - Invoked after container.init is called -- **`onLoad`**: `AsyncHook<[{ id: string; expose: string; pkgNameOrAlias: string; remote: Remote; options: ModuleOptions; origin: FederationHost; exposeModule: any; exposeModuleFactory: any; moduleInstance: Module; }], void>` - - Triggered once a federated module is loaded, allowing access and modification to the exports of the loaded file. -- **`handlePreloadModule`**: `SyncHook<{ id: string; name: string; remoteSnapshot: ModuleInfo; preloadConfig: PreloadRemoteArgs; }, void>` - - Handles preloading logic for federated modules. -- **`errorLoadRemote`**: `AsyncHook<[{ id: string; error: unknown; }], void | unknown>` - - Invoked if loading a federated module fails, enabling custom error handling. -- **`beforeLoadShare`**: `AsyncWaterfallHook<{ pkgName: string; shareInfo?: Shared; - - shared: Options['shared']; origin: FederationHost; }>` - - Called before attempting to load or negotiate shared modules between federated apps. -- **`loadShare`**: `AsyncHook<[FederationHost, string, ShareInfos]>` - - Similar to `onLoad`, but for shared modules. -- **`resolveShare`**: `SyncHook<[{ shareScopeMap: ShareScopeMap; scope: string; pkgName: string; version: string; GlobalFederation: Federation; resolver: () => Shared; }], void>` - - Allows manual resolution of shared module requests. -- **`beforePreloadRemote`**: `AsyncHook<{ preloadOps: Array; options: Options; origin: FederationHost; }>` - - Invoked before any preload logic is executed by the preload handler. -- **`generatePreloadAssets`**: `AsyncHook<[{ origin: FederationHost; preloadOptions: PreloadOptions[number]; remote: Remote; remoteInfo: RemoteInfo; remoteSnapshot: ModuleInfo; globalSnapshot: GlobalModuleInfo; }], Promise>` - - Called for generating preload assets based on configurations. -- **`afterPreloadRemote`**: `AsyncHook<{ preloadOps: Array; options: Options; origin: FederationHost; }>` - - Invoked after the remote modules are preloaded. - -## Plugin System Integration -`FederationHost` utilizes `PluginSystem` for extended capabilities and custom behavior integration, using `FederationRuntimePlugin`. - -## Types and Options - -### `FederationRuntimePlugin` -- **Properties**: - - `name`: `string` - Name of the plugin. - - `version?`: `string` - Optional version of the plugin. - - `CoreLifeCyclePartial`, `SnapshotLifeCycleCyclePartial`, `ModuleLifeCycleCyclePartial`: Partial lifecycle hooks for `FederationHost`, `SnapshotHandler`, and `Module`. - -### `RemoteInfoOptionalVersion` -- **Properties**: - - `name`: `string` - Name of the remote. - - `version?`: `string` - Optional version of the remote. - -### `PreloadRemoteArgs` -- **Properties**: - - `nameOrAlias`: `string` - Name or alias of the remote. - - `exposes?`: `Array` - List of exposed modules. - - `resourceCategory?`: `'all' | 'sync'` - Category of resources. - - `share?`: `boolean` - Flag to share the module. - - `depsRemote?`: `boolean | Array` - Dependencies of the remote. - - `filter?`: `(assetUrl: string) => boolean` - Filter function for assets. diff --git a/packages/runtime/src/plugins/layers-plugin.ts b/packages/runtime/src/plugins/layers-plugin.ts new file mode 100644 index 00000000000..d84dfe1ad42 --- /dev/null +++ b/packages/runtime/src/plugins/layers-plugin.ts @@ -0,0 +1,331 @@ +import type { FederationRuntimePlugin, RemoteEntryExports } from '../type'; +import type { FederationHost } from '../core'; +import type { Options, UserOptions, ShareInfos, RemoteInfo, InitScope, RemoteEntryInitOptions, ShareScopeMap, Remote, Shared, PreloadRemoteArgs, PreloadOptions, CallFrom } from '../type'; +import type { ModuleInfo, GlobalModuleInfo, Manifest, ManifestProvider, PureEntryProvider } from '@module-federation/sdk'; +import type { LoadRemoteMatch } from '../remote'; +import type { Federation } from '../global'; +import type { Module } from '../module'; + +export const layersPlugin: () => FederationRuntimePlugin = function () { + return { + name: 'layers-plugin', + + // 1. Core Initialization Phase + beforeInit({ userOptions, options, origin, shareInfo }: { + userOptions: UserOptions; + options: Options; + origin: FederationHost; + shareInfo: ShareInfos; + }) { + console.log('beforeInit hook triggered', { userOptions, options, shareInfo }); + return { userOptions, options, origin, shareInfo }; + }, + + init({ options, origin }: { + options: Options; + origin: FederationHost; + }) { + console.log('init hook triggered', { options }); + }, + + beforeInitContainer({ shareScope, initScope, remoteEntryInitOptions, remoteInfo, origin }: { + shareScope: ShareScopeMap[string]; + initScope: InitScope; + remoteEntryInitOptions: RemoteEntryInitOptions; + remoteInfo: RemoteInfo; + origin: FederationHost; + }) { + console.log('beforeInitContainer hook triggered', { shareScope, initScope, remoteEntryInitOptions, remoteInfo }); + return { shareScope, initScope, remoteEntryInitOptions, remoteInfo, origin }; + }, + + async initContainer({ shareScope, initScope, remoteEntryInitOptions, remoteInfo, remoteEntryExports, origin, id, remoteSnapshot }: { + shareScope: ShareScopeMap[string]; + initScope: InitScope; + remoteEntryInitOptions: RemoteEntryInitOptions; + remoteInfo: RemoteInfo; + remoteEntryExports: RemoteEntryExports; + origin: FederationHost; + id: string; + remoteSnapshot?: ModuleInfo; + }) { + console.log('initContainer hook triggered', { shareScope, initScope, remoteEntryInitOptions, remoteInfo, remoteEntryExports, id, remoteSnapshot }); + return { shareScope, initScope, remoteEntryInitOptions, remoteInfo, remoteEntryExports, id, remoteSnapshot, origin }; + }, + + // 2. Shared Module Handling Phase + initContainerShareScopeMap({ shareScope, options, origin, scopeName, hostShareScopeMap }: { + shareScope: ShareScopeMap[string]; + options: Options; + origin: FederationHost; + scopeName: string; + hostShareScopeMap?: ShareScopeMap; + }) { + console.log('initContainerShareScopeMap hook triggered', { shareScope, options, scopeName, hostShareScopeMap }); + return { shareScope, options, origin, scopeName, hostShareScopeMap }; + }, + + async beforeLoadShare({ pkgName, shareInfo, shared, origin }: { + pkgName: string; + shareInfo?: Shared; + shared: Options['shared']; + origin: FederationHost; + }) { + + if(shareInfo?.shareConfig.layer) { + shareInfo.scope[0] = `(${shareInfo.shareConfig.layer})${shareInfo.scope[0]})` + } + debugger; + console.log('beforeLoadShare hook triggered', { pkgName, shareInfo, shared }); + return { pkgName, shareInfo, shared, origin }; + }, + + async loadShare(origin: FederationHost, pkgName: string, shareInfo: ShareInfos) { + debugger; + console.log('loadShare hook triggered', { pkgName, shareInfo }); + }, + + resolveShare({ shareScopeMap, scope, pkgName, version, GlobalFederation, resolver }: { + shareScopeMap: ShareScopeMap; + scope: string; + pkgName: string; + version: string; + GlobalFederation: Federation; + resolver: () => Shared | undefined; + }) { + debugger; + console.log('resolveShare hook triggered', { shareScopeMap, scope, pkgName, version, GlobalFederation, resolver }); + + // debugger; + return { shareScopeMap, scope, pkgName, version, GlobalFederation, resolver }; + }, + + async afterResolve(args: LoadRemoteMatch) { + console.log('afterResolve hook triggered', args); + return args; + }, + + // 3. Remote Module Handling Phase + beforeRegisterRemote({ remote, origin }: { + remote: Remote; + origin: FederationHost; + }) { + console.log('beforeRegisterRemote hook triggered', { remote }); + return { remote, origin }; + }, + + registerRemote({ remote, origin }: { + remote: Remote; + origin: FederationHost; + }) { + console.log('registerRemote hook triggered', { remote }); + return { remote, origin }; + }, + + async beforePreloadRemote({ preloadOps, options, origin }: { + preloadOps: Array; + options: Options; + origin: FederationHost; + }) { + console.log('beforePreloadRemote hook triggered', { preloadOps, options }); + }, + + async generatePreloadAssets({ origin, preloadOptions, remote, remoteInfo, remoteSnapshot, globalSnapshot }: { + origin: FederationHost; + preloadOptions: PreloadOptions[number]; + remote: Remote; + remoteInfo: RemoteInfo; + remoteSnapshot: ModuleInfo; + globalSnapshot: GlobalModuleInfo; + }) { + console.log('generatePreloadAssets hook triggered', { preloadOptions, remote, remoteInfo, remoteSnapshot, globalSnapshot }); + return { + cssAssets: [], + jsAssetsWithoutEntry: [], + entryAssets: [] + }; + }, + + async afterPreloadRemote({ preloadOps, options, origin }: { + preloadOps: Array; + options: Options; + origin: FederationHost; + }) { + console.log('afterPreloadRemote hook triggered', { preloadOps, options }); + }, + + async beforeRequest({ id, options, origin }: { + id: string; + options: Options; + origin: FederationHost; + }) { + console.log('beforeRequest hook triggered', { id, options }); + return { id, options, origin }; + }, + + async loadEntry({ loaderHook, remoteInfo, remoteEntryExports }: { + loaderHook: FederationHost['loaderHook']; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports; + }): Promise { + console.log('loadEntry hook triggered', { loaderHook, remoteInfo, remoteEntryExports }); + return remoteEntryExports || { + get: () => async () => ({}), + init: async () => { return; } + }; + }, + + async onLoad({ id, expose, pkgNameOrAlias, remote, options, origin, exposeModule, exposeModuleFactory, moduleInstance }: { + id: string; + expose: string; + pkgNameOrAlias: string; + remote: Remote; + options: { remoteInfo: RemoteInfo; host: FederationHost }; + origin: FederationHost; + exposeModule: any; + exposeModuleFactory: any; + moduleInstance: Module; + }) { + console.log('onLoad hook triggered', { id, expose, pkgNameOrAlias, remote, options, exposeModule, exposeModuleFactory, moduleInstance }); + }, + + handlePreloadModule({ id, name, remote, remoteSnapshot, preloadConfig, origin }: { + id: string; + name: string; + remote: Remote; + remoteSnapshot: ModuleInfo; + preloadConfig: PreloadRemoteArgs; + origin: FederationHost; + }) { + console.log('handlePreloadModule hook triggered', { id, name, remote, remoteSnapshot, preloadConfig }); + }, + + async errorLoadRemote({ id, error, options, from, lifecycle, origin }: { + id: string; + error: unknown; + options?: any; + from: CallFrom; + lifecycle: 'beforeLoadShare' | 'beforeRequest' | 'onLoad'; + origin: FederationHost; + }) { + console.log('errorLoadRemote hook triggered', { id, error, options, from, lifecycle }); + }, + + // 4. Module Factory and Info Phase + getModuleInfo({ target, key }: { + target: Record; + key: any; + }) { + console.log('getModuleInfo hook triggered', { target, key }); + return undefined; + }, + + async getModuleFactory({ remoteEntryExports, expose, moduleInfo }: { + remoteEntryExports: RemoteEntryExports; + expose: string; + moduleInfo: RemoteInfo; + }) { + console.log('getModuleFactory hook triggered', { remoteEntryExports, expose, moduleInfo }); + return undefined; + }, + + // 5. Resource Loading Phase + createScript({ url, attrs }: { + url: string; + attrs?: Record; + }) { + console.log('createScript hook triggered', { url, attrs }); + return undefined; + }, + + createLink({ url, attrs }: { + url: string; + attrs?: Record; + }) { + console.log('createLink hook triggered', { url, attrs }); + return undefined; + }, + + fetch(url: string, init: RequestInit): Promise | void | false { + console.log('fetch hook triggered', { url, init }); + return false; + }, + + async loadEntryError({ getRemoteEntry, origin, remoteInfo, remoteEntryExports, globalLoading, uniqueKey }: { + getRemoteEntry: ({ origin, remoteEntryExports, remoteInfo }: { + origin: FederationHost; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports; + }) => Promise; + origin: FederationHost; + remoteInfo: RemoteInfo; + remoteEntryExports?: RemoteEntryExports; + globalLoading: Record | undefined>; + uniqueKey: string; + }) { + console.log('loadEntryError hook triggered', { getRemoteEntry, remoteInfo, remoteEntryExports, globalLoading, uniqueKey }); + return undefined; + }, + + // 6. Bridge Lifecycle Phase + beforeBridgeRender(args: Record) { + console.log('beforeBridgeRender hook triggered', args); + return undefined; + }, + + afterBridgeRender(args: Record) { + console.log('afterBridgeRender hook triggered', args); + return undefined; + }, + + beforeBridgeDestroy(args: Record) { + console.log('beforeBridgeDestroy hook triggered', args); + return undefined; + }, + + afterBridgeDestroy(args: Record) { + console.log('afterBridgeDestroy hook triggered', args); + return undefined; + }, + + // 7. Snapshot Handling Phase + async beforeLoadRemoteSnapshot({ options, moduleInfo }: { + options: Options; + moduleInfo: Remote; + }) { + console.log('beforeLoadRemoteSnapshot hook triggered', { options, moduleInfo }); + }, + + async loadSnapshot({ options, moduleInfo, hostGlobalSnapshot, globalSnapshot, remoteSnapshot }: { + options: Options; + moduleInfo: Remote; + hostGlobalSnapshot: ModuleInfo | ManifestProvider | PureEntryProvider | undefined; + globalSnapshot: GlobalModuleInfo; + remoteSnapshot?: ModuleInfo | ManifestProvider | PureEntryProvider; + }) { + console.log('loadSnapshot hook triggered', { options, moduleInfo, hostGlobalSnapshot, globalSnapshot, remoteSnapshot }); + return { options, moduleInfo, hostGlobalSnapshot, globalSnapshot, remoteSnapshot }; + }, + + async loadRemoteSnapshot({ options, moduleInfo, manifestJson, manifestUrl, remoteSnapshot, from }: { + options: Options; + moduleInfo: Remote; + manifestJson?: Manifest; + manifestUrl?: string; + remoteSnapshot: ModuleInfo; + from: 'global' | 'manifest'; + }) { + console.log('loadRemoteSnapshot hook triggered', { options, moduleInfo, manifestJson, manifestUrl, remoteSnapshot, from }); + return { options, moduleInfo, manifestJson, manifestUrl, remoteSnapshot, from }; + }, + + async afterLoadSnapshot({ options, moduleInfo, remoteSnapshot }: { + options: Options; + moduleInfo: Remote; + remoteSnapshot: ModuleInfo; + }) { + console.log('afterLoadSnapshot hook triggered', { options, moduleInfo, remoteSnapshot }); + return { options, moduleInfo, remoteSnapshot }; + } + }; +}; diff --git a/packages/runtime/src/shared/index.ts b/packages/runtime/src/shared/index.ts index c1d6728af66..82373a62744 100644 --- a/packages/runtime/src/shared/index.ts +++ b/packages/runtime/src/shared/index.ts @@ -79,17 +79,26 @@ export class SharedHandler { userOptions, ); + // Initialize default scope + if (!this.shareScopeMap[DEFAULT_SCOPE]) { + this.shareScopeMap[DEFAULT_SCOPE] = {}; + } + + // Create layer-specific scopes and register shared modules const sharedKeys = Object.keys(shareInfos); sharedKeys.forEach((sharedKey) => { const sharedVals = shareInfos[sharedKey]; sharedVals.forEach((sharedVal) => { - const registeredShared = getRegisteredShare( - this.shareScopeMap, - sharedKey, - sharedVal, - this.hooks.lifecycle.resolveShare, - ); - if (!registeredShared && sharedVal && sharedVal.lib) { + // Create layer-specific scope if needed + if (sharedVal.shareConfig?.layer) { + const layerScope = `(${sharedVal.shareConfig.layer})${DEFAULT_SCOPE}`; + if (!this.shareScopeMap[layerScope]) { + this.shareScopeMap[layerScope] = {}; + } + } + + // Register in appropriate scopes + if (sharedVal && sharedVal.lib) { this.setShared({ pkgName: sharedKey, lib: sharedVal.lib, @@ -127,6 +136,8 @@ export class SharedHandler { shareInfos: host.options.shared, }); + console.log('shareInfo', shareInfo); + if (shareInfo?.scope) { await Promise.all( shareInfo.scope.map(async (shareScope) => { @@ -494,32 +505,64 @@ export class SharedHandler { }): void { const { version, scope = 'default', ...shareInfo } = shared; const scopes: string[] = Array.isArray(scope) ? scope : [scope]; + + // Initialize scopes scopes.forEach((sc) => { if (!this.shareScopeMap[sc]) { this.shareScopeMap[sc] = {}; } - if (!this.shareScopeMap[sc][pkgName]) { + + // Create layer-specific scope if needed + if (shareInfo.shareConfig?.layer) { + const layerScope = `(${shareInfo.shareConfig.layer})${sc}`; + if (!this.shareScopeMap[layerScope]) { + this.shareScopeMap[layerScope] = {}; + } + if (!this.shareScopeMap[layerScope][pkgName]) { + this.shareScopeMap[layerScope][pkgName] = {}; + } + } else if (!this.shareScopeMap[sc][pkgName]) { + // Only create package object in default scope if no layer this.shareScopeMap[sc][pkgName] = {}; } + }); - if (!this.shareScopeMap[sc][pkgName][version]) { - this.shareScopeMap[sc][pkgName][version] = { - version, - scope: ['default'], - ...shareInfo, - lib, - loaded, - loading, - }; - if (get) { - this.shareScopeMap[sc][pkgName][version].get = get; - } - return; + // Register the share in appropriate scopes + scopes.forEach((sc) => { + const sharedEntry = { + version, + scope: scopes, + ...shareInfo, + lib, + loaded, + loading, + }; + + if (get) { + sharedEntry.get = get; } - const registeredShared = this.shareScopeMap[sc][pkgName][version]; - if (loading && !registeredShared.loading) { - registeredShared.loading = loading; + if (shareInfo.shareConfig?.layer) { + // Register in layer scope only + const layerScope = `(${shareInfo.shareConfig.layer})${sc}`; + if (!this.shareScopeMap[layerScope][pkgName][version]) { + this.shareScopeMap[layerScope][pkgName][version] = sharedEntry; + } else if ( + loading && + !this.shareScopeMap[layerScope][pkgName][version].loading + ) { + this.shareScopeMap[layerScope][pkgName][version].loading = loading; + } + } else { + // Register in original scope + if (!this.shareScopeMap[sc][pkgName][version]) { + this.shareScopeMap[sc][pkgName][version] = sharedEntry; + } else if ( + loading && + !this.shareScopeMap[sc][pkgName][version].loading + ) { + this.shareScopeMap[sc][pkgName][version].loading = loading; + } } }); } diff --git a/packages/runtime/src/type/config.ts b/packages/runtime/src/type/config.ts index 8e7355a6452..80770dc9c99 100644 --- a/packages/runtime/src/type/config.ts +++ b/packages/runtime/src/type/config.ts @@ -50,6 +50,7 @@ export interface SharedConfig { requiredVersion: false | string; eager?: boolean; strictVersion?: boolean; + layer?: string | null; } type SharedBaseArgs = { diff --git a/packages/runtime/src/utils/share.functional.spec.ts b/packages/runtime/src/utils/share.functional.spec.ts new file mode 100644 index 00000000000..c8a3ee86389 --- /dev/null +++ b/packages/runtime/src/utils/share.functional.spec.ts @@ -0,0 +1,1160 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + formatShare, + getRegisteredShare, + getRegisteredShareFromLayer, + findSingletonVersionOrderByVersion, + findSingletonVersionOrderByLoaded, + getFindShareFunction, + getGlobalShareScope, + getTargetSharedOptions, +} from './share'; +import { DEFAULT_SCOPE } from '../constant'; +import { SyncWaterfallHook } from './hooks'; +import type { Shared, ShareScopeMap, ShareInfos, ShareStrategy } from '../type'; + +// Mock console for warning tests +const mockWarn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); +const mockError = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + +describe('share utilities', () => { + describe('formatShare', () => { + it('should format share with get function', () => { + const shareArgs = { + get: () => Promise.resolve(() => ({ version: '1.0.0' })), + version: '1.0.0', + shareConfig: { + requiredVersion: '^1.0.0', + }, + }; + + const result = formatShare(shareArgs, 'test-host', 'test-pkg'); + + expect(result).toMatchObject({ + version: '1.0.0', + from: 'test-host', + scope: ['default'], + strategy: 'version-first', + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + }); + expect(typeof result.get).toBe('function'); + }); + + it('should format share with lib', () => { + const shareArgs = { + lib: () => ({ version: '1.0.0' }), + version: '1.0.0', + shareConfig: { + requiredVersion: '^1.0.0', + }, + }; + + const result = formatShare(shareArgs, 'test-host', 'test-pkg'); + + expect(result).toMatchObject({ + version: '1.0.0', + from: 'test-host', + scope: ['default'], + loaded: true, + strategy: 'version-first', + }); + expect(typeof result.get).toBe('function'); + }); + + it('should format share with custom scope and layer', () => { + const shareArgs = { + lib: () => ({ version: '1.0.0' }), + version: '1.0.0', + scope: ['custom'], + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^1.0.0', + }, + }; + + const result = formatShare(shareArgs, 'test-host', 'test-pkg'); + + expect(result).toMatchObject({ + version: '1.0.0', + from: 'test-host', + scope: ['custom'], + loaded: true, + strategy: 'version-first', + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^1.0.0', + }, + }); + }); + + it('should handle string scope by converting to array', () => { + const shareArgs = { + lib: () => ({ version: '1.0.0' }), + version: '1.0.0', + scope: 'custom', + shareConfig: { + requiredVersion: '^1.0.0', + }, + }; + + const result = formatShare(shareArgs, 'test-host', 'test-pkg'); + + expect(result.scope).toEqual(['custom']); + }); + + it('should warn when using deprecated strategy in shareArgs', () => { + const shareArgs = { + lib: () => ({ version: '1.0.0' }), + version: '1.0.0', + strategy: 'loaded-first' as const, + shareConfig: { + requiredVersion: '^1.0.0', + }, + }; + + formatShare(shareArgs, 'test-host', 'test-pkg'); + + expect(mockWarn).toHaveBeenCalledWith( + expect.stringContaining('shared.strategy is deprecated'), + ); + }); + + it('should use provided strategy parameter over shareArgs strategy', () => { + const shareArgs = { + lib: () => ({ version: '1.0.0' }), + version: '1.0.0', + strategy: 'loaded-first' as const, + shareConfig: { + requiredVersion: '^1.0.0', + }, + }; + + const result = formatShare( + shareArgs, + 'test-host', + 'test-pkg', + 'version-first', + ); + + expect(result.strategy).toBe('version-first'); + }); + + it('should throw error when neither get nor lib is provided', () => { + const shareArgs = { + version: '1.0.0', + shareConfig: { + requiredVersion: '^1.0.0', + }, + }; + + const result = formatShare(shareArgs, 'test-host', 'test-pkg'); + + expect(typeof result.get).toBe('function'); + expect(result.get()).rejects.toThrow('Can not get shared'); + }); + }); + + describe('getRegisteredShare', () => { + const resolveShare = new SyncWaterfallHook<{ + shareScopeMap: ShareScopeMap; + scope: string; + pkgName: string; + version: string; + GlobalFederation: any; + resolver: () => Shared | undefined; + }>('resolveShare'); + + it('should return undefined if no share scope map', () => { + const result = getRegisteredShare( + {} as ShareScopeMap, + 'react', + { + version: '1.0.0', + shareConfig: { + requiredVersion: '^1.0.0', + singleton: false, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + from: 'test', + deps: [], + useIn: [], + loading: null, + strategy: 'version-first', + } as Shared, + resolveShare, + ); + + expect(result).toBeUndefined(); + }); + + it('should find share in default scope when no layer', () => { + const mockShare: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + from: 'test', + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.0'); + expect(result?.from).toBe('test'); + }); + + it('should check layer scope first when layer exists', () => { + const mockShare16: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const mockShare17: Shared = { + version: '17.0.0', + lib: () => ({ version: '17.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^17.0.0', + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare16, + }, + }, + '(base)default': { + react: { + '17.0.0': mockShare17, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('17.0.0'); // Should get from layer scope + }); + + it('should handle multiple scopes', () => { + const mockShare16: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const mockShare161: Shared = { + version: '16.0.1', + lib: () => ({ version: '16.0.1' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: ['custom'], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare16, + }, + }, + custom: { + react: { + '16.0.1': mockShare161, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: ['default', 'custom'], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.1'); // Should get highest version across scopes + }); + + it('should handle loaded-first strategy', () => { + const mockShare16: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'loaded-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + loaded: true, + }; + + const mockShare17: Shared = { + version: '17.0.0', + lib: () => ({ version: '17.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^17.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'loaded-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + loaded: false, + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare16, + '17.0.0': mockShare17, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'loaded-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.0'); // Should get loaded version even though lower + }); + + it('should warn but not error when version mismatch with strictVersion false', () => { + const mockShare: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare, + }, + }, + }; + + const shareInfo: Shared = { + version: '17.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^17.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(mockWarn).toHaveBeenCalledWith( + expect.stringContaining('does not satisfy'), + ); + expect(mockError).not.toHaveBeenCalled(); + }); + + it('should error when version mismatch with strictVersion true', () => { + const mockShare: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: true, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare, + }, + }, + }; + + const shareInfo: Shared = { + version: '17.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^17.0.0', + eager: false, + strictVersion: true, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(mockError).toHaveBeenCalledWith( + expect.stringContaining('does not satisfy'), + ); + }); + + it('should handle requiredVersion=false by returning any version', () => { + const mockShare: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: false, + singleton: false, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare, + }, + }, + }; + + const shareInfo: Shared = { + version: '17.0.0', + shareConfig: { + singleton: false, + requiredVersion: false, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.0'); + }); + + it('should handle requiredVersion="*" by returning any version', () => { + const mockShare: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '*', + singleton: false, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': mockShare, + }, + }, + }; + + const shareInfo: Shared = { + version: '17.0.0', + shareConfig: { + singleton: false, + requiredVersion: '*', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShare( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.0'); + }); + }); + + describe('getRegisteredShareFromLayer', () => { + const resolveShare = new SyncWaterfallHook<{ + shareScopeMap: ShareScopeMap; + scope: string; + pkgName: string; + version: string; + GlobalFederation: any; + resolver: () => Shared | undefined; + }>('resolveShare'); + + it('should return undefined if layer scope does not exist', () => { + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + } as Shared, + }, + }, + }; + + const result = getRegisteredShareFromLayer( + shareScopeMap, + 'react', + { + version: '16.0.0', + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + } as Shared, + resolveShare, + ); + + expect(result).toBeUndefined(); + }); + + it('should find share in layer scope', () => { + const mockShare: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: {}, + '(base)default': { + react: { + '16.0.0': mockShare, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShareFromLayer( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.0'); + expect(result?.shareConfig.layer).toBe('base'); + }); + + it('should handle multiple scopes with layer', () => { + const mockShare16: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const mockShare17: Shared = { + version: '17.0.0', + lib: () => ({ version: '17.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^17.0.0', + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: ['custom'], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: {}, + custom: {}, + '(base)default': { + react: { + '16.0.0': mockShare16, + }, + }, + '(base)custom': { + react: { + '17.0.0': mockShare17, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE, 'custom'], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShareFromLayer( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('17.0.0'); // Should get highest version across layer scopes + }); + + it('should respect version requirements within layer', () => { + const mockShare16: Shared = { + version: '16.0.0', + lib: () => ({ version: '16.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^16.0.0', + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const mockShare17: Shared = { + version: '17.0.0', + lib: () => ({ version: '17.0.0' }), + get: () => Promise.resolve(() => ({})), + shareConfig: { + requiredVersion: '^17.0.0', + singleton: true, + eager: false, + strictVersion: false, + layer: 'base', + }, + scope: [DEFAULT_SCOPE], + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test', + }; + + const shareScopeMap: ShareScopeMap = { + default: {}, + '(base)default': { + react: { + '16.0.0': mockShare16, + '17.0.0': mockShare17, + }, + }, + }; + + const shareInfo: Shared = { + version: '16.0.0', + shareConfig: { + layer: 'base', + singleton: true, + requiredVersion: '^16.0.0', + eager: false, + strictVersion: false, + }, + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + strategy: 'version-first', + deps: [], + useIn: [], + loading: null, + from: 'test-consumer', + }; + + const result = getRegisteredShareFromLayer( + shareScopeMap, + 'react', + shareInfo, + resolveShare, + ); + + expect(result).toBeDefined(); + expect(result?.version).toBe('16.0.0'); // Should get version that satisfies requirement + }); + }); + + describe('findSingletonVersionOrderByVersion', () => { + it('should find highest version when none are loaded', () => { + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': { + version: '16.0.0', + loaded: false, + } as Shared, + '17.0.0': { + version: '17.0.0', + loaded: false, + } as Shared, + }, + }, + }; + + const result = findSingletonVersionOrderByVersion( + shareScopeMap, + 'default', + 'react', + ); + + expect(result).toBe('17.0.0'); + }); + }); + + describe('findSingletonVersionOrderByLoaded', () => { + it('should prioritize loaded versions', () => { + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': { + version: '16.0.0', + loaded: true, + } as Shared, + '17.0.0': { + version: '17.0.0', + loaded: false, + } as Shared, + }, + }, + }; + + const result = findSingletonVersionOrderByLoaded( + shareScopeMap, + 'default', + 'react', + ); + + expect(result).toBe('16.0.0'); + }); + + it('should choose higher version when multiple are loaded', () => { + const shareScopeMap: ShareScopeMap = { + default: { + react: { + '16.0.0': { + version: '16.0.0', + loaded: true, + } as Shared, + '17.0.0': { + version: '17.0.0', + loaded: true, + } as Shared, + }, + }, + }; + + const result = findSingletonVersionOrderByLoaded( + shareScopeMap, + 'default', + 'react', + ); + + expect(result).toBe('17.0.0'); + }); + }); + + describe('getFindShareFunction', () => { + it('should return findSingletonVersionOrderByLoaded for loaded-first strategy', () => { + const result = getFindShareFunction('loaded-first'); + expect(result).toBe(findSingletonVersionOrderByLoaded); + }); + + it('should return findSingletonVersionOrderByVersion for version-first strategy', () => { + const result = getFindShareFunction('version-first'); + expect(result).toBe(findSingletonVersionOrderByVersion); + }); + }); + + describe('getGlobalShareScope', () => { + it('should return global share scope map', () => { + const result = getGlobalShareScope(); + expect(result).toBeDefined(); + expect(typeof result).toBe('object'); + }); + }); + + describe('getTargetSharedOptions', () => { + it('should get target shared options', () => { + const shareInfos: ShareInfos = { + react: [ + { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + from: 'test', + scope: [DEFAULT_SCOPE], + get: () => Promise.resolve(() => ({})), + deps: [], + useIn: [], + loading: null, + strategy: 'version-first' as ShareStrategy, + }, + ], + }; + + const options = { + pkgName: 'react', + extraOptions: { + customShareInfo: { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + shareInfos, + }; + + const result = getTargetSharedOptions(options); + expect(result).toEqual({ + singleton: true, + requiredVersion: '^16.0.0', + }); + }); + + it('should return undefined if no matching options', () => { + const options = { + pkgName: 'react', + extraOptions: { + customShareInfo: { + version: '16.0.0', + shareConfig: { + singleton: true, + requiredVersion: '^16.0.0', + }, + }, + }, + shareInfos: {} as ShareInfos, + }; + + const result = getTargetSharedOptions(options); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/packages/runtime/src/utils/share.ts b/packages/runtime/src/utils/share.ts index 66c8c9547eb..45418cf8891 100644 --- a/packages/runtime/src/utils/share.ts +++ b/packages/runtime/src/utils/share.ts @@ -53,7 +53,9 @@ export function formatShare( ...shareArgs.shareConfig, }, get, + // DO NOT CHANGE loaded: shareArgs?.loaded || 'lib' in shareArgs ? true : undefined, + // DO NOT CHANGE version: shareArgs.version ?? '0', scope: Array.isArray(shareArgs.scope) ? shareArgs.scope @@ -156,7 +158,7 @@ const isLoading = (shared: Shared) => { return Boolean(shared.loading); }; -function findSingletonVersionOrderByVersion( +export function findSingletonVersionOrderByVersion( shareScopeMap: ShareScopeMap, scope: string, pkgName: string, @@ -169,7 +171,7 @@ function findSingletonVersionOrderByVersion( return findVersion(shareScopeMap[scope][pkgName], callback); } -function findSingletonVersionOrderByLoaded( +export function findSingletonVersionOrderByLoaded( shareScopeMap: ShareScopeMap, scope: string, pkgName: string, @@ -196,13 +198,175 @@ function findSingletonVersionOrderByLoaded( return findVersion(shareScopeMap[scope][pkgName], callback); } -function getFindShareFunction(strategy: Shared['strategy']) { +export function getFindShareFunction(strategy: Shared['strategy']) { if (strategy === 'loaded-first') { return findSingletonVersionOrderByLoaded; } return findSingletonVersionOrderByVersion; } +export function getRegisteredShareFromLayer( + localShareScopeMap: ShareScopeMap, + pkgName: string, + shareInfo: Shared, + resolveShare: SyncWaterfallHook<{ + shareScopeMap: ShareScopeMap; + scope: string; + pkgName: string; + version: string; + GlobalFederation: Federation; + resolver: () => Shared | undefined; + }>, +): Shared | void { + const { shareConfig, scope = DEFAULT_SCOPE, strategy } = shareInfo; + const scopes = Array.isArray(scope) ? scope : [scope]; + + if (!shareConfig?.layer) { + return; + } + + for (const sc of scopes) { + const compositeScope = `(${shareConfig.layer})${sc}`; + console.log('checking composite scope:', compositeScope); + + if (localShareScopeMap[compositeScope]?.[pkgName]) { + const findShareFunction = getFindShareFunction(strategy); + const maxOrSingletonVersion = findShareFunction( + localShareScopeMap, + compositeScope, + pkgName, + ); + console.log('found version in composite scope:', maxOrSingletonVersion); + + const defaultResolver = () => { + if (shareConfig.singleton) { + if ( + typeof shareConfig.requiredVersion === 'string' && + !satisfy(maxOrSingletonVersion, shareConfig.requiredVersion) + ) { + const msg = `Version ${maxOrSingletonVersion} from ${ + maxOrSingletonVersion && + localShareScopeMap[compositeScope][pkgName][maxOrSingletonVersion] + .from + } of shared singleton module ${pkgName} does not satisfy the requirement of ${ + shareInfo.from + } which needs ${shareConfig.requiredVersion})`; + + if (shareConfig.strictVersion) { + error(msg); + } else { + warn(msg); + } + } + return localShareScopeMap[compositeScope][pkgName][ + maxOrSingletonVersion + ]; + } else { + if ( + shareConfig.requiredVersion === false || + shareConfig.requiredVersion === '*' + ) { + return localShareScopeMap[compositeScope][pkgName][ + maxOrSingletonVersion + ]; + } + if (satisfy(maxOrSingletonVersion, shareConfig.requiredVersion)) { + return localShareScopeMap[compositeScope][pkgName][ + maxOrSingletonVersion + ]; + } + + for (const [versionKey, versionValue] of Object.entries( + localShareScopeMap[compositeScope][pkgName], + )) { + if (satisfy(versionKey, shareConfig.requiredVersion)) { + return versionValue; + } + } + return undefined; + } + }; + + const params = { + shareScopeMap: localShareScopeMap, + scope: compositeScope, + pkgName, + version: maxOrSingletonVersion, + GlobalFederation: Global.__FEDERATION__, + resolver: defaultResolver, + }; + const resolveShared = resolveShare.emit(params) || params; + const result = resolveShared.resolver(); + if (result) return result; + } + + // If no matching share found in layer scope, check default scope + if (localShareScopeMap[sc]?.[pkgName]) { + const findShareFunction = getFindShareFunction(strategy); + const maxOrSingletonVersion = findShareFunction( + localShareScopeMap, + sc, + pkgName, + ); + console.log('found version in default scope:', maxOrSingletonVersion); + + const defaultResolver = () => { + if (shareConfig.singleton) { + if ( + typeof shareConfig.requiredVersion === 'string' && + !satisfy(maxOrSingletonVersion, shareConfig.requiredVersion) + ) { + const msg = `Version ${maxOrSingletonVersion} from ${ + maxOrSingletonVersion && + localShareScopeMap[sc][pkgName][maxOrSingletonVersion].from + } of shared singleton module ${pkgName} does not satisfy the requirement of ${ + shareInfo.from + } which needs ${shareConfig.requiredVersion})`; + + if (shareConfig.strictVersion) { + error(msg); + } else { + warn(msg); + } + } + return localShareScopeMap[sc][pkgName][maxOrSingletonVersion]; + } else { + if ( + shareConfig.requiredVersion === false || + shareConfig.requiredVersion === '*' + ) { + return localShareScopeMap[sc][pkgName][maxOrSingletonVersion]; + } + if (satisfy(maxOrSingletonVersion, shareConfig.requiredVersion)) { + return localShareScopeMap[sc][pkgName][maxOrSingletonVersion]; + } + + for (const [versionKey, versionValue] of Object.entries( + localShareScopeMap[sc][pkgName], + )) { + if (satisfy(versionKey, shareConfig.requiredVersion)) { + return versionValue; + } + } + return undefined; + } + }; + + const params = { + shareScopeMap: localShareScopeMap, + scope: sc, + pkgName, + version: maxOrSingletonVersion, + GlobalFederation: Global.__FEDERATION__, + resolver: defaultResolver, + }; + const resolveShared = resolveShare.emit(params) || params; + const result = resolveShared.resolver(); + if (result) return result; + } + } +} + export function getRegisteredShare( localShareScopeMap: ShareScopeMap, pkgName: string, @@ -221,7 +385,28 @@ export function getRegisteredShare( } const { shareConfig, scope = DEFAULT_SCOPE, strategy } = shareInfo; const scopes = Array.isArray(scope) ? scope : [scope]; + + console.log( + 'getRegisteredShare - scopes:', + scopes, + 'layer:', + shareConfig?.layer, + ); + + // Check layer scopes first if layer exists + if (shareConfig?.layer) { + const layerResult = getRegisteredShareFromLayer( + localShareScopeMap, + pkgName, + shareInfo, + resolveShare, + ); + if (layerResult) return layerResult; + } + + // Then check original scopes for (const sc of scopes) { + console.log('checking scope:', sc); if ( shareConfig && localShareScopeMap[sc] && @@ -234,6 +419,7 @@ export function getRegisteredShare( sc, pkgName, ); + console.log('found version in scope:', maxOrSingletonVersion); //@ts-ignore const defaultResolver = () => { diff --git a/packages/webpack-bundler-runtime/src/initializeSharing.ts b/packages/webpack-bundler-runtime/src/initializeSharing.ts index 9db5bdd0bd8..515a2cc8e9d 100644 --- a/packages/webpack-bundler-runtime/src/initializeSharing.ts +++ b/packages/webpack-bundler-runtime/src/initializeSharing.ts @@ -9,6 +9,7 @@ export function initializeSharing({ initTokens, initScope, }: InitializeSharingOptions): Promise | boolean | void { + console.log('InitializeSharing', shareScopeName, initScope); if (!initScope) initScope = []; const mfInstance = webpackRequire.federation.instance!; diff --git a/packages/webpack-bundler-runtime/src/installInitialConsumes.ts b/packages/webpack-bundler-runtime/src/installInitialConsumes.ts index 5abcdaaab3a..656d1f8c685 100644 --- a/packages/webpack-bundler-runtime/src/installInitialConsumes.ts +++ b/packages/webpack-bundler-runtime/src/installInitialConsumes.ts @@ -4,13 +4,13 @@ import { } from './types'; function handleInitialConsumes(options: HandleInitialConsumesOptions) { const { moduleId, moduleToHandlerMapping, webpackRequire } = options; - +console.log('Initialized module', options); const federationInstance = webpackRequire.federation.instance; if (!federationInstance) { throw new Error('Federation instance not found!'); } const { shareKey, shareInfo } = moduleToHandlerMapping[moduleId]; - + debugger; try { return federationInstance.loadShareSync(shareKey, { customShareInfo: shareInfo, @@ -32,6 +32,8 @@ export function installInitialConsumes(options: InstallInitialConsumesOptions) { initialConsumes, } = options; + console.log('Initialized initial consumes', installedModules); + initialConsumes.forEach((id) => { webpackRequire.m[id] = (module) => { // Handle scenario when module is used synchronously