diff --git a/package-lock.json b/package-lock.json index c007052..dad3e4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -836,11 +836,11 @@ "dev": true }, "node_modules/@module-federation/runtime": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.0.10.tgz", - "integrity": "sha512-M57xoGcN64JDyNwerOoAlUyyplA1fGKlGWWc6sw5A+eRb+mt/Nj2Yh5g3+C3iZ0b3eoLtDe3qlX12UuddmySPg==", + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.1.15.tgz", + "integrity": "sha512-iB21/US6UdA8Lzt0kIi10n6Fxz9YLuLjx1UJbmsWCd0TFEXctMhvbIKA5NmZq27DPDVSDW9LPuO5rbaii7ycBg==", "dependencies": { - "@module-federation/sdk": "0.0.10" + "@module-federation/sdk": "0.1.15" } }, "node_modules/@module-federation/runtime-tools": { @@ -869,9 +869,9 @@ "dev": true }, "node_modules/@module-federation/sdk": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.0.10.tgz", - "integrity": "sha512-qHK7FoOSd8HCBal0bSBCn/BB1e5XXwdfwzPt4ACP/o4YyHTnIIavX9qyA9bufvHltVgP/JL5jpBOpK7sdZ1bYQ==" + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.1.15.tgz", + "integrity": "sha512-alN2O0+BVmFRpPDQStj1ff4DRlV7H1XTruEh3zmwBbVqWy45lXChC+KmJn1mgAoL4faeaPrlMEjePhbwTPUCjw==" }, "node_modules/@module-federation/webpack-bundler-runtime": { "version": "0.0.8", @@ -15388,11 +15388,11 @@ } }, "packages/rollup-plugin-module-federation": { - "version": "1.10.2", + "version": "1.10.3", "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.0.10", - "@module-federation/sdk": "0.0.10", + "@module-federation/runtime": "0.1.15", + "@module-federation/sdk": "0.1.15", "estree-walker": "3.0.3", "magic-string": "0.30.1", "semver": "7.5.4" @@ -15405,6 +15405,7 @@ "@rollup/plugin-terser": "0.4.3", "@rollup/plugin-typescript": "11.1.5", "@semantic-release/git": "10.0.1", + "@types/estree": "1.0.5", "@vitest/coverage-v8": "1.3.1", "rollup": "3.23.0", "rollup-plugin-copy": "3.4.0", diff --git a/packages/rollup-plugin-module-federation/package.json b/packages/rollup-plugin-module-federation/package.json index 21eae3d..2b4d550 100644 --- a/packages/rollup-plugin-module-federation/package.json +++ b/packages/rollup-plugin-module-federation/package.json @@ -61,8 +61,8 @@ ] }, "dependencies": { - "@module-federation/runtime": "0.0.10", - "@module-federation/sdk": "0.0.10", + "@module-federation/runtime": "0.1.15", + "@module-federation/sdk": "0.1.15", "estree-walker": "3.0.3", "magic-string": "0.30.1", "semver": "7.5.4" @@ -82,7 +82,8 @@ "tslib": "2.6.2", "type-fest": "4.7.0", "typescript": "5.2.2", - "vitest": "1.3.1" + "vitest": "1.3.1", + "@types/estree": "1.0.5" }, "engines": { "node": ">= 18.0.0" diff --git a/packages/rollup-plugin-module-federation/src/constants.ts b/packages/rollup-plugin-module-federation/src/constants.ts index ee25be8..7908a2a 100644 --- a/packages/rollup-plugin-module-federation/src/constants.ts +++ b/packages/rollup-plugin-module-federation/src/constants.ts @@ -1 +1,2 @@ export const PACKAGE_JSON: string = 'package.json'; +export const DEFAULT_CONTAINER_NAME: string = 'default_container_name'; diff --git a/packages/rollup-plugin-module-federation/src/index.ts b/packages/rollup-plugin-module-federation/src/index.ts index 61c986a..887e2cb 100644 --- a/packages/rollup-plugin-module-federation/src/index.ts +++ b/packages/rollup-plugin-module-federation/src/index.ts @@ -19,7 +19,7 @@ import { import { PACKAGE_JSON } from './constants'; import type { ImportDeclaration, ExportNamedDeclaration, Node } from 'estree'; -import type { ModuleFederationPluginOptions } from '../types'; +import { moduleFederationPlugin } from '@module-federation/sdk'; import type { PackageJson } from 'type-fest'; import type { Plugin, ManualChunksOption, AcornNode } from 'rollup'; @@ -31,6 +31,7 @@ import { FederatedModuleType, ModuleVersionInfo, } from './types'; +import { ShareArgs } from '@module-federation/runtime/types'; const IMPORTS_TO_FEDERATED_IMPORTS_NODES = { ImportDeclaration: 'ImportDeclaration', @@ -194,7 +195,7 @@ export function getFederatedImportStatementForNode( } export default function federation( - federationConfig: ModuleFederationPluginOptions, + federationConfig: moduleFederationPlugin.ModuleFederationPluginOptions, ): Plugin { const { name, filename, shareScope = 'default' } = federationConfig; @@ -456,6 +457,22 @@ export default function federation( */ remoteEntryCode.append( Object.entries(initConfig?.shared ?? {}) + .reduce>( + (allSharedConfigs, [moduleNameOrPath, sharedConfigs]) => { + if (Array.isArray(sharedConfigs)) { + return allSharedConfigs.concat( + sharedConfigs.map((sharedConfigForPkg) => [ + moduleNameOrPath, + sharedConfigForPkg, + ]), + ); + } + return allSharedConfigs.concat([ + [moduleNameOrPath, sharedConfigs], + ]); + }, + [], + ) .filter( ([_, sharedConfigForPkg]) => sharedConfigForPkg.shareConfig?.eager, @@ -496,36 +513,43 @@ export default function federation( remotes: ${JSON.stringify(initConfig.remotes)}, shared: { ${Object.entries(initConfig.shared ?? {}) - .map(([moduleNameOrPath, sharedConfigForPkg]) => { - return ` - '${moduleNameOrPath}': { - ${ - /** - * We inject the entire object as json and then remote the starting and ending curly braces - * This is to add further keys to the object. - */ - JSON.stringify(sharedConfigForPkg).replace( - /^\{|\}$/g, - '', - ) - }, - version: '${getVersionForModule(moduleNameOrPath)}', - ${ - /** - * If the dependency is declared as a import: false, then we don't need to provide it to the initConfig. - * QUESTION: How does one even support import: false with this ? - * Bug: https://github.com/module-federation/universe/issues/2020 - */ - !shared[moduleNameOrPath]?.import - ? `get: () => Promise.resolve().then(() => () => null),` - : /** - * TODO: Convert this to a lib and re-write eager shared imports to loadShareSync() + .map(([moduleNameOrPath, sharedConfigs]) => { + return Array.isArray(sharedConfigs) + ? sharedConfigs + : [sharedConfigs] + .map((sharedConfigForPkg) => { + return ` + '${moduleNameOrPath}': { + ${ + /** + * We inject the entire object as json and then remote the starting and ending curly braces + * This is to add further keys to the object. */ - sharedConfigForPkg.shareConfig?.eager - ? `get: () => Promise.resolve(${FEDERATED_EAGER_SHARED}${moduleNameOrPath}).then((module) => () => module),` - : `get: () => import('${moduleNameOrPath}').then((module) => () => module),` - } - },`; + JSON.stringify(sharedConfigForPkg).replace( + /^\{|\}$/g, + '', + ) + }, + version: '${getVersionForModule(moduleNameOrPath)}', + ${ + /** + * If the dependency is declared as a import: false, then we don't need to provide it to the initConfig. + * QUESTION: How does one even support import: false with this ? + * Bug: https://github.com/module-federation/universe/issues/2020 + */ + !shared[moduleNameOrPath]?.import + ? `get: () => Promise.resolve().then(() => () => null),` + : /** + * TODO: Convert this to a lib and re-write eager shared imports to loadShareSync() + */ + sharedConfigForPkg.shareConfig?.eager + ? `get: () => Promise.resolve(${FEDERATED_EAGER_SHARED}${moduleNameOrPath}).then((module) => () => module),` + : `get: () => import('${moduleNameOrPath}').then((module) => () => module),` + } + }, + `; + }) + .join(''); }) .join('')} } diff --git a/packages/rollup-plugin-module-federation/src/types/index.d.ts b/packages/rollup-plugin-module-federation/src/types/index.ts similarity index 81% rename from packages/rollup-plugin-module-federation/src/types/index.d.ts rename to packages/rollup-plugin-module-federation/src/types/index.ts index a969257..59f44d8 100644 --- a/packages/rollup-plugin-module-federation/src/types/index.d.ts +++ b/packages/rollup-plugin-module-federation/src/types/index.ts @@ -1,10 +1,16 @@ -import { ExposesConfig, SharedConfig, RemotesConfig } from '../../types'; -import type { ShareArgs } from '@module-federation/runtime/dist/type.cjs.js'; +import { moduleFederationPlugin } from '@module-federation/sdk'; +import type { ShareArgs } from '@module-federation/runtime/types'; +import type { + ImportDeclaration, + ImportExpression, + ExportNamedDeclaration, + ExportAllDeclaration, +} from 'estree'; /** * We rewrite the type for SharedObject to be that of the most verbose definition. */ export type SharedObject = { - [index: string]: SharedConfig & { + [index: string]: moduleFederationPlugin.SharedConfig & { import: string | false; }; }; @@ -13,14 +19,14 @@ export type SharedObject = { * We rewrite the type for ExposesObject to be that of the most verbose definition. */ export type ExposesObject = { - [index: string]: ExposesConfig; + [index: string]: moduleFederationPlugin.ExposesConfig; }; /** * We rewrite the type for RemotesObject to be that of the most verbose definition. */ export type RemotesObject = { - [index: string]: RemotesConfig; + [index: string]: moduleFederationPlugin.RemotesConfig; }; export type ShareInfo = { diff --git a/packages/rollup-plugin-module-federation/src/utils.ts b/packages/rollup-plugin-module-federation/src/utils.ts index 4896e5d..898b436 100644 --- a/packages/rollup-plugin-module-federation/src/utils.ts +++ b/packages/rollup-plugin-module-federation/src/utils.ts @@ -1,14 +1,14 @@ import { dirname, sep } from 'node:path'; import { existsSync, readFileSync, lstatSync } from 'node:fs'; -import { PACKAGE_JSON } from './constants'; +import { PACKAGE_JSON, DEFAULT_CONTAINER_NAME } from './constants'; import { generateExposeFilename, generateShareFilename, + moduleFederationPlugin, } from '@module-federation/sdk'; -import type { UserOptions } from '@module-federation/runtime/dist/types.cjs.js'; +import type { UserOptions } from '@module-federation/runtime/types'; import type { PackageJson } from 'type-fest'; -import type { Exposes, Remotes, Shared } from '../types'; import type { SharedObject, ExposesObject, @@ -70,7 +70,9 @@ export function getNearestPackageJson(path: string): PackageJson | null { return getNearestPackageJson(parentDir); } -export function getSharedConfig(shared: Shared): SharedObject { +export function getSharedConfig( + shared: moduleFederationPlugin.Shared, +): SharedObject { if (Array.isArray(shared)) { return shared.reduce( (sharedObject, sharedEntity): SharedObject => { @@ -128,7 +130,9 @@ export function getSharedConfig(shared: Shared): SharedObject { } } -export function getExposesConfig(exposes: Exposes): ExposesObject { +export function getExposesConfig( + exposes: moduleFederationPlugin.Exposes, +): ExposesObject { if (Array.isArray(exposes)) { return exposes.reduce( (exposedModules, exposedEntity): ExposesObject => { @@ -184,7 +188,9 @@ export function getExposesConfig(exposes: Exposes): ExposesObject { } } -export function getRemotesConfig(remotes: Remotes): RemotesObject { +export function getRemotesConfig( + remotes: moduleFederationPlugin.Remotes, +): RemotesObject { if (Array.isArray(remotes)) { return remotes.reduce( (remoteModules, remoteEntity): RemotesObject => { @@ -259,16 +265,16 @@ export function getRequiredVersionForModule( } export function getInitConfig( - name: string, + name: string | undefined, shared: SharedObject, remotes: RemotesObject, federatedModuleInfo: Record, remoteType: string, ): UserOptions { return { - name, - shared: Object.entries(shared).reduce( - (sharedConfig, [pkgName, sharedConfigForPkg]): ShareInfo => { + name: name ?? DEFAULT_CONTAINER_NAME, + shared: Object.entries(shared).reduce( + (sharedConfig, [pkgName, sharedConfigForPkg]) => { const sharedOptionForPkg = { version: sharedConfigForPkg.version as string, shareConfig: { @@ -286,11 +292,9 @@ export function getInitConfig( /** * If its a package for which the user has specified import: false, then we load whatever version is given to us from the shared scope. */ - ...(sharedConfigForPkg.import === false - ? { - strategy: 'loaded-first', - } - : {}), + strategy: sharedConfigForPkg.import + ? 'version-first' + : 'loaded-first', }; return { ...sharedConfig, @@ -301,14 +305,12 @@ export function getInitConfig( }, {}, ), - /** - * TODO: Find a type definition of how plugins can be injected during build time. - */ plugins: [], remotes: Object.entries(remotes).map(([remoteName, remoteConfig]) => { return { name: remoteName, - entry: remoteConfig.external, + // TODO: Remove this type coercion once we get an answer from module federation team. + entry: remoteConfig.external as string, shareScope: remoteConfig.shareScope, type: remoteType === 'module' || remoteType === 'import' ? 'esm' : 'global', diff --git a/packages/rollup-plugin-module-federation/tests/utils.test.js b/packages/rollup-plugin-module-federation/tests/utils.test.js index 4c27925..164ccf6 100644 --- a/packages/rollup-plugin-module-federation/tests/utils.test.js +++ b/packages/rollup-plugin-module-federation/tests/utils.test.js @@ -402,6 +402,7 @@ describe('getInitConfig', () => { }, scope: undefined, lib: expect.any(Function), + strategy: 'version-first', }, sharedPkg2: { version: undefined, diff --git a/packages/rollup-plugin-module-federation/types/index.d.ts b/packages/rollup-plugin-module-federation/types/index.d.ts deleted file mode 100644 index 76d275d..0000000 --- a/packages/rollup-plugin-module-federation/types/index.d.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Adapted and modified from: https://github.com/webpack/webpack/blob/main/types.d.ts - * The following options have been modified/removed: - * 1. library has been removed. - * 2. name has been made a required property. Previously it was optional. - * 3. In ExposesConfig, import has been made to string. Previously it was string | string[] - * 4. In Remoted config, externals has been made to string. Previously it was string | string[] - */ -export declare interface ModuleFederationPluginOptions { - /** - * Modules that should be exposed by this container. When provided, property name is used as public name, otherwise public name is automatically inferred from request. - */ - exposes?: (string | ExposesObject)[] | ExposesObject; - - /** - * The filename of the container as relative path inside the `output.path` directory. - */ - filename?: string; - - /** - * The name of the container. - */ - name: string; - - /** - * The external type of the remote containers. - */ - remoteType?: - | 'import' - | 'var' - | 'module' - | 'assign' - | 'this' - | 'window' - | 'self' - | 'global' - | 'commonjs' - | 'commonjs2' - | 'commonjs-module' - | 'commonjs-static' - | 'amd' - | 'amd-require' - | 'umd' - | 'umd2' - | 'jsonp' - | 'system' - | 'promise' - | 'script' - | 'node-commonjs'; - - /** - * Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location. - */ - remotes?: (string | RemotesObject)[] | RemotesObject; - - /** - * The name of the runtime chunk. If set a runtime chunk with this name is created or an existing entrypoint is used as runtime. - */ - runtime?: string | false; - - /** - * Share scope name used for all shared modules (defaults to 'default'). - */ - shareScope?: string; - - /** - * Modules that should be shared in the share scope. When provided, property names are used to match requested modules in this compilation. - */ - shared?: (string | SharedObject)[] | SharedObject; - /** - * Runtime plugin file paths or package name. - */ - runtimePlugins?: string[]; -} - -export type Exposes = (string | ExposesObject)[] | ExposesObject; - -/** - * Advanced configuration for modules that should be exposed by this container. - */ -export declare interface ExposesConfig { - /** - * Request to a module that should be exposed by this container. - */ - import: string; - - /** - * Custom chunk name for the exposed module. - */ - name?: string; -} - -/** - * Modules that should be exposed by this container. Property names are used as public paths. - */ -export declare interface ExposesObject { - [index: string]: string | ExposesConfig | string[]; -} - -export type Remotes = (string | RemotesObject)[] | RemotesObject; - -/** - * Advanced configuration for container locations from which modules should be resolved and loaded at runtime. - */ -export declare interface RemotesConfig { - /** - * Container locations from which modules should be resolved and loaded at runtime. - */ - external: string; - - /** - * The name of the share scope shared with this remote. - */ - shareScope?: string; -} - -/** - * Container locations from which modules should be resolved and loaded at runtime. Property names are used as request scopes. - */ -export declare interface RemotesObject { - [index: string]: string | RemotesConfig | string[]; -} - -export type Shared = (string | SharedObject)[] | SharedObject; - -/** - * Advanced configuration for modules that should be shared in the share scope. - */ -export declare interface SharedConfig { - /** - * Include the provided and fallback module directly instead behind an async request. This allows to use this shared module in initial load too. All possible shared modules need to be eager too. - */ - eager?: boolean; - - /** - * Provided module that should be provided to share scope. Also acts as fallback module if no shared module is found in share scope or version isn't valid. Defaults to the property name. - */ - import?: string | false; - - /** - * Package name to determine required version from description file. This is only needed when package name can't be automatically determined from request. - */ - packageName?: string; - - /** - * Version requirement from module in share scope. - */ - requiredVersion?: string | false; - - /** - * Module is looked up under this key from the share scope. - */ - shareKey?: string; - - /** - * Share scope name. - */ - shareScope?: string; - - /** - * Allow only a single version of the shared module in share scope (disabled by default). - */ - singleton?: boolean; - - /** - * Do not accept shared module if version is not valid (defaults to yes, if local fallback module is available and shared module is not a singleton, otherwise no, has no effect if there is no required version specified). - */ - strictVersion?: boolean; - - /** - * Version of the provided module. Will replace lower matching versions, but not higher. - */ - version?: string | false; -} - -/** - * Modules that should be shared in the share scope. Property names are used to match requested modules in this compilation. Relative requests are resolved, module requests are matched unresolved, absolute paths will match resolved requests. A trailing slash will match all requests with this prefix. In this case shareKey must also have a trailing slash. - */ -export declare interface SharedObject { - [index: string]: string | SharedConfig; -}