From 3aaa97ef3d79cae7d5d9da104efbe923282e2fe5 Mon Sep 17 00:00:00 2001 From: Vojtech Szocs Date: Wed, 29 Apr 2026 19:07:57 +0000 Subject: [PATCH] Improve handling of aliased shared modules --- .../scripts/package-definitions.ts | 28 +++++++-------- .../__tests__/plugin-shared-modules.spec.ts | 29 ++++++++++++++++ .../src/runtime/plugin-init.ts | 2 +- .../src/runtime/plugin-shared-modules.ts | 34 +++++++++++++++++-- .../src/shared-modules/shared-modules-meta.ts | 17 ++++++++-- .../src/webpack/ConsoleRemotePlugin.ts | 12 ++++--- 6 files changed, 96 insertions(+), 26 deletions(-) create mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-shared-modules.spec.ts diff --git a/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts b/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts index 317f6bf5fb9..e9a1a0d5b5f 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts @@ -76,15 +76,7 @@ const parseDeps = ( missingDepCallback: MissingDependencyCallback, ) => { const srcDeps = { ...pkg.devDependencies, ...pkg.dependencies }; - - depNames - // Console does not have an explicit react-router-dom(-v5-compat) dependency. - // react-router-dom(-v5-compat) shared module impl. delegates to react-router. - .filter((name) => name !== 'react-router-dom-v5-compat') - .filter((name) => name !== 'react-router-dom') - .filter((name) => !srcDeps[name]) - .forEach(missingDepCallback); - + depNames.filter((name) => !srcDeps[name]).forEach(missingDepCallback); return _.pick(srcDeps, depNames); }; @@ -97,12 +89,20 @@ const parseDepsAs = ( const parseSharedModuleDeps = (pkg: PackageJson, missingDepCallback: MissingDependencyCallback) => parseDeps( pkg, - sharedPluginModules.filter( - (m) => - m !== '@openshift/dynamic-plugin-sdk' && // This is a direct SDK dependency as well as a shared module + sharedPluginModules.filter((m) => { + const { allowFallback, aliased } = getSharedModuleMetadata(m); + + return ( + // Exclude Console plugin SDK shared modules. !m.startsWith('@openshift-console/') && - !getSharedModuleMetadata(m).allowFallback, - ), + // This is a Console plugin SDK dependency and a shared module. + m !== '@openshift/dynamic-plugin-sdk' && + // Exclude modules for which a plugin provided fallback version is disallowed. + // Also exclude modules whose implementation is aliased to another module. + !allowFallback && + !aliased + ); + }), missingDepCallback, ); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-shared-modules.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-shared-modules.spec.ts new file mode 100644 index 00000000000..b7275067eb0 --- /dev/null +++ b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-shared-modules.spec.ts @@ -0,0 +1,29 @@ +import type { getSharedScope } from '../plugin-shared-modules'; +import { monkeyPatchSharedScope } from '../plugin-shared-modules'; + +describe('monkeyPatchSharedScope', () => { + it('adds aliased modules to share scope object', () => { + const getModule = jest.fn(); + + const testScope: ReturnType = { + 'react-router': { + '7.13.1': { from: 'openshift-console', eager: true, loaded: 1, get: getModule }, + }, + }; + + monkeyPatchSharedScope(testScope); + + expect(Object.keys(testScope)).toEqual([ + 'react-router', + 'react-router-dom', + 'react-router-dom-v5-compat', + ]); + + expect(testScope['react-router']).toEqual({ + '7.13.1': { from: 'openshift-console', eager: true, loaded: 1, get: getModule }, + }); + + expect(testScope['react-router-dom']).toEqual(testScope['react-router']); + expect(testScope['react-router-dom-v5-compat']).toEqual(testScope['react-router']); + }); +}); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-init.ts b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-init.ts index 5c3e1af2107..a3bebc2d47b 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-init.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-init.ts @@ -82,7 +82,7 @@ const registerLegacyPluginEntryCallback = () => { // eslint-disable-next-line no-console console.warn( - `[DEPRECATION WARNING] ${pluginName} was built for an older version of Console and may not work correctly in this version.`, + `[WARNING] ${pluginName} was built for an older version of Console and may not work correctly in this version.`, ); window[REMOTE_ENTRY_CALLBACK](patchedPluginName, entryModule); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts index d0bd0a5e873..7941e7c72d2 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-shared-modules.ts @@ -12,6 +12,34 @@ const SHARED_SCOPE_NAME = 'default'; */ export const initSharedScope = async () => __webpack_init_sharing__(SHARED_SCOPE_NAME); +/** + * webpack provided type of `__webpack_share_scopes__` object is incorrect; at runtime, + * each shared module comes with one or more version(s) with each version scoped to its + * own object, for example: + * + * ```js + * { + * [SCOPE_NAME]: { + * 'react': { + * '18.3.1': { + * from: 'openshift-console', + * eager: true, + * loaded: 1, + * get: moduleFactoryFunction, + * } + * } + * } + * } + * ``` + */ +type WebpackShareScopes = { + [scopeName: string]: { + [moduleName: string]: { + [moduleVersion: string]: typeof __webpack_share_scopes__[string][string]; + }; + }; +}; + /** * Get the webpack share scope object. */ @@ -20,7 +48,8 @@ export const getSharedScope = () => { throw new Error('Attempt to access share scope object before its initialization'); } - return __webpack_share_scopes__[SHARED_SCOPE_NAME]; + // TODO: Remove this type cast once __webpack_share_scopes__ object type is fixed + return (__webpack_share_scopes__[SHARED_SCOPE_NAME] as unknown) as WebpackShareScopes[string]; }; /** @@ -29,8 +58,7 @@ export const getSharedScope = () => { * - add `react-router-dom-v5-compat` module aliased to `react-router` * - add `react-router-dom` module aliased to `react-router` */ -export const monkeyPatchSharedScope = () => { - const scope = getSharedScope(); +export const monkeyPatchSharedScope = (scope = getSharedScope()) => { scope['react-router-dom'] = scope['react-router']; scope['react-router-dom-v5-compat'] = scope['react-router']; }; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules/shared-modules-meta.ts b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules/shared-modules-meta.ts index 0f24e0b72c6..7945afaea5c 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules/shared-modules-meta.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules/shared-modules-meta.ts @@ -17,6 +17,16 @@ type SharedModuleMetadata = Partial<{ */ allowFallback: boolean; + /** + * If `true`, this module's implementation is aliased to another module. + * + * Plugins should avoid using aliased modules due to risk of potential skew between + * aliased vs actual module code. + * + * @default false + */ + aliased: boolean; + /** * A message describing the deprecation, if the module has been deprecated. * @@ -57,8 +67,8 @@ const sharedPluginModulesMetadata: Record { +const getCompileTimeSharedModuleWarnings = (pkg: ConsolePluginPackageJSON): string[] => { const warnings: string[] = []; sharedPluginModules.forEach((moduleName) => { - const { deprecated } = getSharedModuleMetadata(moduleName); + const { deprecated, aliased } = getSharedModuleMetadata(moduleName); - if (deprecated && hasPackageDependency(pkg, moduleName)) { + if ((deprecated || aliased) && hasPackageDependency(pkg, moduleName)) { warnings.push( - `[DEPRECATION WARNING] Console provided shared module ${moduleName} has been deprecated: ${deprecated}`, + deprecated + ? `[WARNING] Console provided shared module ${moduleName} has been deprecated: ${deprecated}` + : `[WARNING] Console provided shared module ${moduleName} is aliased, beware of potential skew between aliased vs actual module code`, ); } }); @@ -476,7 +478,7 @@ export class ConsoleRemotePlugin implements WebpackPluginInstance { } } - getDeprecatedSharedModuleWarnings(this.pkg).forEach((message) => { + getCompileTimeSharedModuleWarnings(this.pkg).forEach((message) => { compilation.warnings.push(new compiler.webpack.WebpackError(message)); }); });