diff --git a/e2e/vite-webpack-rspack/tests/index.spec.ts b/e2e/vite-webpack-rspack/tests/index.spec.ts index 1e7b5bc..7d834f4 100644 --- a/e2e/vite-webpack-rspack/tests/index.spec.ts +++ b/e2e/vite-webpack-rspack/tests/index.spec.ts @@ -131,6 +131,32 @@ test.describe('Dynamic remote', () => { await expect(signUpBanner).toBeVisible(); await expect(specialPromoBanner).not.toBeVisible(); }); + + test('verifies shared lodash dependency', async ({ page, baseURL }) => { + await page.goto(baseURL!); + const showAdToggle = page.getByRole('checkbox', { name: 'Show Dynamic Ad', exact: true }); + + // Check that lodash version is displayed in SpecialPromo banner + await showAdToggle.check({ force: true }); + + const specialPromoBanner = page.getByRole('heading', { level: 2, name: 'Up to 50% off!', exact: true }); + await expect(specialPromoBanner).toBeVisible(); + + const lodashVersionDisplay = page.getByTestId('lodash-version-display'); + await expect(lodashVersionDisplay).toBeVisible(); + const versionText = await lodashVersionDisplay.textContent(); + expect(versionText).toMatch(/Shared lodash v\d+\.\d+\.\d+/); + + // Toggle off and on again to check SignUpBanner + await showAdToggle.uncheck({ force: true }); + await showAdToggle.check({ force: true }); + + const signUpBanner = page.getByRole('heading', { level: 2, name: 'Sign up now!', exact: true }); + await expect(signUpBanner).toBeVisible(); + await expect(lodashVersionDisplay).toBeVisible(); + const versionText2 = await lodashVersionDisplay.textContent(); + expect(versionText2).toMatch(/Shared lodash v\d+\.\d+\.\d+/); + }); }); test.describe('Tests remote', () => { diff --git a/examples/vite-webpack-rspack/dynamic-remote/package.json b/examples/vite-webpack-rspack/dynamic-remote/package.json index f2675f8..a931d71 100644 --- a/examples/vite-webpack-rspack/dynamic-remote/package.json +++ b/examples/vite-webpack-rspack/dynamic-remote/package.json @@ -10,9 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@module-federation/vite": "workspace:*", + "lodash": "^4.17.21", "react": "^18.3.1", - "react-dom": "^18.3.1", - "@module-federation/vite": "workspace:*" + "react-dom": "^18.3.1" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/examples/vite-webpack-rspack/dynamic-remote/src/SignUpBanner.jsx b/examples/vite-webpack-rspack/dynamic-remote/src/SignUpBanner.jsx index 00724b8..da4e876 100644 --- a/examples/vite-webpack-rspack/dynamic-remote/src/SignUpBanner.jsx +++ b/examples/vite-webpack-rspack/dynamic-remote/src/SignUpBanner.jsx @@ -1,9 +1,12 @@ +import _ from 'lodash'; + const SignUpBanner = () => { return (

Sign up now!

Get started with our amazing service today.

+

Shared lodash v{_.VERSION}

Random diff --git a/examples/vite-webpack-rspack/dynamic-remote/src/SpecialPromo.jsx b/examples/vite-webpack-rspack/dynamic-remote/src/SpecialPromo.jsx index 8ba3969..19a71ea 100644 --- a/examples/vite-webpack-rspack/dynamic-remote/src/SpecialPromo.jsx +++ b/examples/vite-webpack-rspack/dynamic-remote/src/SpecialPromo.jsx @@ -1,8 +1,11 @@ +import _ from 'lodash'; + const SpecialPromo = () => { return (

Up to 50% off!

+

Shared lodash v{_.VERSION}

Only for a limited time.

Random diff --git a/examples/vite-webpack-rspack/dynamic-remote/vite.config.js b/examples/vite-webpack-rspack/dynamic-remote/vite.config.js index f9ab454..3fe2b4a 100644 --- a/examples/vite-webpack-rspack/dynamic-remote/vite.config.js +++ b/examples/vite-webpack-rspack/dynamic-remote/vite.config.js @@ -14,7 +14,11 @@ export default defineConfig({ './SignUpBanner': './src/SignUpBanner.jsx', './SpecialPromo': './src/SpecialPromo.jsx', }, - shared: ['react', 'react-dom'], + shared: { + react: { singleton: true }, + 'react-dom': { singleton: true }, + lodash: { singleton: true, import: false }, + }, }), ], server: { diff --git a/examples/vite-webpack-rspack/host/package.json b/examples/vite-webpack-rspack/host/package.json index 91949cf..ae818a4 100644 --- a/examples/vite-webpack-rspack/host/package.json +++ b/examples/vite-webpack-rspack/host/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@module-federation/vite": "workspace:*", + "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/examples/vite-webpack-rspack/host/src/App.tsx b/examples/vite-webpack-rspack/host/src/App.tsx index 525fe7e..76b2ac8 100644 --- a/examples/vite-webpack-rspack/host/src/App.tsx +++ b/examples/vite-webpack-rspack/host/src/App.tsx @@ -7,6 +7,9 @@ import { Toggle } from './components/Toggle'; import { useDynamicImport } from './hooks/useDynamicImport'; import './index.css'; +import _ from 'lodash'; +_.VERSION; + const RemoteProduct = lazy( () => // @ts-ignore diff --git a/examples/vite-webpack-rspack/host/vite.config.js b/examples/vite-webpack-rspack/host/vite.config.js index 3b2c265..99b63ee 100644 --- a/examples/vite-webpack-rspack/host/vite.config.js +++ b/examples/vite-webpack-rspack/host/vite.config.js @@ -22,7 +22,7 @@ const mfConfig = { type: 'module', }, }, - shared: ['react', 'react-dom'], + shared: ['react', 'react-dom', 'lodash'], }; // https://vitejs.dev/config/ @@ -38,5 +38,6 @@ export default defineConfig({ ], build: { target: 'chrome89', + minify: false, }, }); diff --git a/package.json b/package.json index 43eca39..849117c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "homepage": "https://github.com/module-federation/vite#readme", "packageManager": "pnpm@9.1.3", "dependencies": { - "@module-federation/runtime": "^0.17.1", + "@module-federation/runtime": "^0.18.3", + "@module-federation/sdk": "^0.18.3", "@rollup/pluginutils": "^5.1.0", "defu": "^6.1.4", "estree-walker": "^2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1fa349..8b9e007 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,8 +8,11 @@ importers: .: dependencies: '@module-federation/runtime': - specifier: ^0.17.1 - version: 0.17.1 + specifier: ^0.18.3 + version: 0.18.4 + '@module-federation/sdk': + specifier: ^0.18.3 + version: 0.18.3 '@rollup/pluginutils': specifier: ^5.1.0 version: 5.1.0(rollup@4.47.1) @@ -269,6 +272,9 @@ importers: '@module-federation/vite': specifier: workspace:* version: link:../../.. + lodash: + specifier: ^4.17.21 + version: 4.17.21 react: specifier: ^18.3.1 version: 18.3.1 @@ -303,6 +309,9 @@ importers: '@module-federation/vite': specifier: workspace:* version: link:../../.. + lodash: + specifier: ^4.17.21 + version: 4.17.21 react: specifier: ^18.3.1 version: 18.3.1 @@ -2613,10 +2622,10 @@ packages: webpack: optional: true - '@module-federation/error-codes@0.17.1': + '@module-federation/error-codes@0.18.4': resolution: { - integrity: sha512-n6Elm4qKSjwAPxLUGtwnl7qt4y1dxB8OpSgVvXBIzqI9p27a3ZXshLPLnumlpPg1Qudaj8sLnSnFtt9yGpt5yQ==, + integrity: sha512-cpLsqL8du9CfTTCKvXbRg93ALF+lklqHnuPryhbwVEQg2eYo6CMoMQ6Eb7kJhLigUABIDujbHD01SvBbASGkeQ==, } '@module-federation/managers@0.2.5': @@ -2637,10 +2646,10 @@ packages: integrity: sha512-Rvk4KTPn9KqM84ub3N8Ze1uC7oSJejlC4SG9JxUrr1yvkJh1Ej3SGWpeHyS7trBVWeJItCFqXAlAD2RnFIcjXQ==, } - '@module-federation/runtime-core@0.17.1': + '@module-federation/runtime-core@0.18.4': resolution: { - integrity: sha512-LCtIFuKgWPQ3E+13OyrVpuTPOWBMI/Ggwsq1Q874YeT8Px28b8tJRCj09DjyRFyhpSPyV/uG80T6iXPAUoLIfQ==, + integrity: sha512-LGGlFXlNeTbIGBFDiOvg0zz4jBWCGPqQatXdKx7mylXhDij7YmwbuW19oenX+P1fGhmoBUBM5WndmR87U66qWA==, } '@module-federation/runtime-tools@0.1.6': @@ -2667,10 +2676,10 @@ packages: integrity: sha512-nj6a+yJ+QxmcE89qmrTl4lphBIoAds0PFPVGnqLRWflwAP88jrCcrrTqRhARegkFDL+wE9AE04+h6jzlbIfMKg==, } - '@module-federation/runtime@0.17.1': + '@module-federation/runtime@0.18.4': resolution: { - integrity: sha512-vKEN32MvUbpeuB/s6UXfkHDZ9N5jFyDDJnj83UTJ8n4N1jHIJu9VZ6Yi4/Ac8cfdvU8UIK9bIbfVXWbUYZUDsw==, + integrity: sha512-2et6p7pjGRHzpmrW425jt/BiAU7QHgkZtbQB7pj01eQ8qx6SloFEBk9ODnV8/ztSm9H2T3d8GxXA6/9xVOslmQ==, } '@module-federation/runtime@0.2.5': @@ -2691,10 +2700,16 @@ packages: integrity: sha512-qifXpyYLM7abUeEOIfv0oTkguZgRZuwh89YOAYIZJlkP6QbRG7DJMQvtM8X2yHXm9PTk0IYNnOJH0vNQCo6auQ==, } - '@module-federation/sdk@0.17.1': + '@module-federation/sdk@0.18.3': resolution: { - integrity: sha512-nlUcN6UTEi+3HWF+k8wPy7gH0yUOmCT+xNatihkIVR9REAnr7BUvHFGlPJmx7WEbLPL46+zJUbtQHvLzXwFhng==, + integrity: sha512-tlBgF5pKXoiZ5hGRgafOpsktt0iafdjoH2O85ywPqvDGVK0DzfP8hs4qdUBJlKulP5PZoBtgTe7UiqyTbKJ7YQ==, + } + + '@module-federation/sdk@0.18.4': + resolution: + { + integrity: sha512-dErzOlX+E3HS2Sg1m12Hi9nCnfvQPuIvlq9N47KxrbT2TIU3KKYc9q/Ua+QWqxfTyMVFpbNDwFMJ1R/w/gYf4A==, } '@module-federation/sdk@0.2.5': @@ -14965,7 +14980,7 @@ snapshots: - supports-color - utf-8-validate - '@module-federation/error-codes@0.17.1': {} + '@module-federation/error-codes@0.18.4': {} '@module-federation/managers@0.2.5': dependencies: @@ -15004,10 +15019,10 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/runtime-core@0.17.1': + '@module-federation/runtime-core@0.18.4': dependencies: - '@module-federation/error-codes': 0.17.1 - '@module-federation/sdk': 0.17.1 + '@module-federation/error-codes': 0.18.4 + '@module-federation/sdk': 0.18.4 '@module-federation/runtime-tools@0.1.6': dependencies: @@ -15028,11 +15043,11 @@ snapshots: dependencies: '@module-federation/sdk': 0.1.6 - '@module-federation/runtime@0.17.1': + '@module-federation/runtime@0.18.4': dependencies: - '@module-federation/error-codes': 0.17.1 - '@module-federation/runtime-core': 0.17.1 - '@module-federation/sdk': 0.17.1 + '@module-federation/error-codes': 0.18.4 + '@module-federation/runtime-core': 0.18.4 + '@module-federation/sdk': 0.18.4 '@module-federation/runtime@0.2.5': dependencies: @@ -15044,7 +15059,9 @@ snapshots: '@module-federation/sdk@0.1.6': {} - '@module-federation/sdk@0.17.1': {} + '@module-federation/sdk@0.18.3': {} + + '@module-federation/sdk@0.18.4': {} '@module-federation/sdk@0.2.5': {} diff --git a/src/utils/normalizeModuleFederationOptions.ts b/src/utils/normalizeModuleFederationOptions.ts index 929da7a..b36c082 100644 --- a/src/utils/normalizeModuleFederationOptions.ts +++ b/src/utils/normalizeModuleFederationOptions.ts @@ -1,4 +1,5 @@ import { SharedConfig, ShareStrategy } from '@module-federation/runtime/types'; +import type { sharePlugin } from '@module-federation/sdk'; export type RemoteEntryType = | 'var' @@ -104,7 +105,7 @@ export interface ShareItem { version: string | undefined; scope: string; from: string; - shareConfig: SharedConfig; + shareConfig: SharedConfig & sharePlugin.SharedConfig; } function removePathFromNpmPackage(packageString: string): string { @@ -157,6 +158,7 @@ function normalizeShareItem( | string | { name: string; + import: sharePlugin.SharedConfig['import']; version?: string; shareScope?: string; singleton?: boolean; @@ -192,6 +194,7 @@ function normalizeShareItem( scope: 'default', from: '', shareConfig: { + import: undefined, singleton: false, requiredVersion: version ? `^${version}` : '*', }, @@ -203,6 +206,7 @@ function normalizeShareItem( version: shareItem.version || version, scope: shareItem.shareScope || 'default', shareConfig: { + import: typeof shareItem === 'object' ? shareItem.import : undefined, singleton: shareItem.singleton || false, requiredVersion: shareItem.requiredVersion || (version ? `^${version}` : '*'), strictVersion: !!shareItem.strictVersion, @@ -289,6 +293,7 @@ export type ModuleFederationOptions = { singleton?: boolean; requiredVersion?: string; strictVersion?: boolean; + import?: sharePlugin.SharedConfig['import']; } > | undefined; diff --git a/src/virtualModules/virtualRemoteEntry.ts b/src/virtualModules/virtualRemoteEntry.ts index affd661..ef4e712 100644 --- a/src/virtualModules/virtualRemoteEntry.ts +++ b/src/virtualModules/virtualRemoteEntry.ts @@ -38,17 +38,23 @@ export function writeLocalSharedImportMap() { export function generateLocalSharedImportMap() { const options = getNormalizeModuleFederationOptions(); return ` + import {loadShare} from "@module-federation/runtime"; const importMap = { ${Array.from(getUsedShares()) .sort() - .map( - (pkg) => ` + .map((pkg) => { + const shareItem = getNormalizeShareItem(pkg); + return ` ${JSON.stringify(pkg)}: async () => { - let pkg = await import("${getPreBuildLibImportId(pkg)}") - return pkg + ${ + shareItem?.shareConfig.import === false + ? `throw new Error(\`Shared module '\${${JSON.stringify(pkg)}}' must be provided by host\`);` + : `let pkg = await import("${getPreBuildLibImportId(pkg)}"); + return pkg;` + } } - ` - ) + `; + }) .join(',')} } const usedShared = { @@ -65,8 +71,11 @@ export function generateLocalSharedImportMap() { loaded: false, from: ${JSON.stringify(options.name)}, async get () { + if (${shareItem.shareConfig.import === false}) { + throw new Error(\`Shared module '\${${JSON.stringify(key)}}' must be provided by host\`); + } usedShared[${JSON.stringify(key)}].loaded = true - const {${JSON.stringify(key)}: pkgDynamicImport} = importMap + const {${JSON.stringify(key)}: pkgDynamicImport} = importMap const res = await pkgDynamicImport() const exportModule = {...res} // All npm packages pre-built by vite will be converted to esm @@ -80,7 +89,8 @@ export function generateLocalSharedImportMap() { }, shareConfig: { singleton: ${shareItem.shareConfig.singleton}, - requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)} + requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}, + ${shareItem.shareConfig.import === false ? 'import: false,' : ''} } } `; @@ -178,7 +188,7 @@ const hostAutoInitModule = new VirtualModule('hostAutoInit', HOST_AUTO_INIT_TAG) export function writeHostAutoInit() { hostAutoInitModule.writeSync(` const remoteEntryPromise = import("${REMOTE_ENTRY_ID}") - // __tla only serves as a hack for vite-plugin-top-level-await. + // __tla only serves as a hack for vite-plugin-top-level-await. Promise.resolve(remoteEntryPromise) .then(remoteEntry => { return Promise.resolve(remoteEntry.__tla)