From f8132b6bfc0a44650d7bba83f35ca5b428545799 Mon Sep 17 00:00:00 2001 From: Vojtech Szocs Date: Wed, 10 Nov 2021 17:09:36 +0100 Subject: [PATCH] Adapt webpack 5 related code in dynamic plugin SDK --- dynamic-demo-plugin/yarn.lock | 4 +- .../console-dynamic-plugin-sdk/package.json | 4 +- .../scripts/package-definitions.ts | 2 +- .../src/__tests__/shared-modules-init.spec.ts | 31 +++++ .../__tests__/shared-modules-override.spec.ts | 17 --- .../runtime/__tests__/plugin-loader.spec.ts | 26 ++-- .../src/runtime/plugin-loader.ts | 10 +- .../src/shared-modules-init.ts | 58 +++++++++ .../src/shared-modules-override.ts | 44 ------- .../src/shared-modules.ts | 44 ++----- .../console-dynamic-plugin-sdk/src/types.ts | 29 +++-- .../src/utils/test-utils.ts | 1 + .../src/webpack/ConsoleAssetPlugin.ts | 117 +++++++++++------- .../src/webpack/ConsoleRemotePlugin.ts | 114 +++++++++-------- frontend/yarn.lock | 62 +++++----- 15 files changed, 298 insertions(+), 265 deletions(-) create mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-init.spec.ts delete mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-override.spec.ts create mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-init.ts delete mode 100644 frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-override.ts diff --git a/dynamic-demo-plugin/yarn.lock b/dynamic-demo-plugin/yarn.lock index a07c1adcf9c7..7b3d1019a621 100644 --- a/dynamic-demo-plugin/yarn.lock +++ b/dynamic-demo-plugin/yarn.lock @@ -76,7 +76,7 @@ lodash "^4.17.21" read-pkg "5.x" semver "6.x" - webpack "^5.53.0" + webpack "5.x" "@openshift-console/dynamic-plugin-sdk@file:../frontend/packages/console-dynamic-plugin-sdk/dist/core": version "0.0.0-fixed" @@ -3407,7 +3407,7 @@ webpack-sources@^3.2.0: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.1.tgz#251a7d9720d75ada1469ca07dbb62f3641a05b6d" integrity sha512-t6BMVLQ0AkjBOoRTZgqrWm7xbXMBzD+XDq2EZ96+vMfn3qKgsvdXZhbPZ4ElUOpdv4u+iiGe+w3+J75iy/bYGA== -webpack@5.x, webpack@^5.53.0: +webpack@5.x: version "5.62.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.62.1.tgz#06f09b56a7b1bb13ed5137ad4b118358a90c9505" integrity sha512-jNLtnWChS2CMZ7vqWtztv0G6fYB5hz11Zsadp5tE7e4/66zVDj7/KUeQZOsOl8Hz5KrLJH1h2eIDl6AnlyE12Q== diff --git a/frontend/packages/console-dynamic-plugin-sdk/package.json b/frontend/packages/console-dynamic-plugin-sdk/package.json index 8ce2fef9bf16..fd6cfa4bcd05 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/package.json +++ b/frontend/packages/console-dynamic-plugin-sdk/package.json @@ -22,7 +22,7 @@ "fs-extra": "9.x", "ts-json-schema-generator": "0.91.0", "tsutils": "3.x", - "typescript": "~4.2.3", - "webpack": "^5.53.0" + "typescript": "4.2.x", + "webpack": "5.x" } } 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 504b17b1f62b..3e6c580c8f22 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/scripts/package-definitions.ts @@ -53,7 +53,7 @@ const parseSharedModuleDeps = ( ) => parseDeps( pkg, - Object.keys(sharedPluginModules).filter((m) => !m.startsWith('@openshift-console/')), + sharedPluginModules.filter((m) => !m.startsWith('@openshift-console/')), missingDepCallback, ); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-init.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-init.spec.ts new file mode 100644 index 000000000000..c859e0b1aa8b --- /dev/null +++ b/frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-init.spec.ts @@ -0,0 +1,31 @@ +import { sharedPluginModules } from '../shared-modules'; +import { initSharedPluginModules } from '../shared-modules-init'; +import { getEntryModuleMocks } from '../utils/test-utils'; + +describe('initSharedPluginModules', () => { + const expectSameValues = (arr1: string[], arr2: string[]) => { + expect(new Set(arr1)).toEqual(new Set(arr2)); + }; + + it('is consistent with sharedPluginModules definition', () => { + const [, entryModule] = getEntryModuleMocks({}); + + initSharedPluginModules(entryModule); + + expect(entryModule.init).toHaveBeenCalledTimes(1); + + expectSameValues(Object.keys(entryModule.init.mock.calls[0][0]), sharedPluginModules); + }); + + it('supports plugins built with an older version of plugin SDK', () => { + const [, entryModule] = getEntryModuleMocks({}); + entryModule.override = jest.fn(); + + initSharedPluginModules(entryModule); + + expect(entryModule.override).toHaveBeenCalledTimes(1); + expect(entryModule.init).not.toHaveBeenCalled(); + + expectSameValues(Object.keys(entryModule.override.mock.calls[0][0]), sharedPluginModules); + }); +}); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-override.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-override.spec.ts deleted file mode 100644 index c0aa83687526..000000000000 --- a/frontend/packages/console-dynamic-plugin-sdk/src/__tests__/shared-modules-override.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sharedPluginModules } from '../shared-modules'; -import { overrideSharedModules } from '../shared-modules-override'; -import { getEntryModuleMocks } from '../utils/test-utils'; - -describe('overrideSharedModules', () => { - it('is consistent with sharedPluginModules', () => { - const [, entryModule] = getEntryModuleMocks({}); - - overrideSharedModules(entryModule); - - expect(entryModule.init.mock.calls.length).toBe(1); - - expect(new Set(Object.keys(sharedPluginModules))).toEqual( - new Set(Object.keys(entryModule.init.mock.calls[0][0])), - ); - }); -}); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-loader.spec.ts b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-loader.spec.ts index 026cabe56130..781a4c7cb63a 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-loader.spec.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/__tests__/plugin-loader.spec.ts @@ -171,19 +171,19 @@ describe('window.loadPluginEntry', () => { const [, entryModule] = getEntryModuleMocks({}); const { pluginMap } = getStateForTestPurposes(); - const overrideSharedModules = jest.fn(); + const initSharedPluginModules = jest.fn(); const resolveEncodedCodeRefs = jest.fn(() => resolvedExtensions); pluginMap.set(getPluginID(manifest), { manifest, entryCallbackFired: false }); getPluginEntryCallback( pluginStore, - overrideSharedModules, + initSharedPluginModules, resolveEncodedCodeRefs, )('Test@1.2.3', entryModule); expect(pluginMap.get('Test@1.2.3').entryCallbackFired).toBe(true); - expect(overrideSharedModules).toHaveBeenCalledWith(entryModule); + expect(initSharedPluginModules).toHaveBeenCalledWith(entryModule); expect(resolveEncodedCodeRefs).toHaveBeenCalledWith( manifest.extensions, @@ -202,17 +202,17 @@ describe('window.loadPluginEntry', () => { const [, entryModule] = getEntryModuleMocks({}); const { pluginMap } = getStateForTestPurposes(); - const overrideSharedModules = jest.fn(); + const initSharedPluginModules = jest.fn(); const resolveEncodedCodeRefs = jest.fn(() => []); getPluginEntryCallback( pluginStore, - overrideSharedModules, + initSharedPluginModules, resolveEncodedCodeRefs, )('Test@1.2.3', entryModule); expect(pluginMap.size).toBe(0); - expect(overrideSharedModules).not.toHaveBeenCalled(); + expect(initSharedPluginModules).not.toHaveBeenCalled(); expect(resolveEncodedCodeRefs).not.toHaveBeenCalled(); expect(addDynamicPlugin).not.toHaveBeenCalled(); }); @@ -225,25 +225,25 @@ describe('window.loadPluginEntry', () => { const [, entryModule] = getEntryModuleMocks({}); const { pluginMap } = getStateForTestPurposes(); - const overrideSharedModules = jest.fn(); + const initSharedPluginModules = jest.fn(); const resolveEncodedCodeRefs = jest.fn(() => []); pluginMap.set(getPluginID(manifest), { manifest, entryCallbackFired: false }); getPluginEntryCallback( pluginStore, - overrideSharedModules, + initSharedPluginModules, resolveEncodedCodeRefs, )('Test@1.2.3', entryModule); getPluginEntryCallback( pluginStore, - overrideSharedModules, + initSharedPluginModules, resolveEncodedCodeRefs, )('Test@1.2.3', entryModule); expect(pluginMap.size).toBe(1); - expect(overrideSharedModules).toHaveBeenCalledTimes(1); + expect(initSharedPluginModules).toHaveBeenCalledTimes(1); expect(resolveEncodedCodeRefs).toHaveBeenCalledTimes(1); expect(addDynamicPlugin).toHaveBeenCalledTimes(1); }); @@ -256,7 +256,7 @@ describe('window.loadPluginEntry', () => { const [, entryModule] = getEntryModuleMocks({}); const { pluginMap } = getStateForTestPurposes(); - const overrideSharedModules = jest.fn(() => { + const initSharedPluginModules = jest.fn(() => { throw new Error('boom'); }); const resolveEncodedCodeRefs = jest.fn(() => []); @@ -265,12 +265,12 @@ describe('window.loadPluginEntry', () => { getPluginEntryCallback( pluginStore, - overrideSharedModules, + initSharedPluginModules, resolveEncodedCodeRefs, )('Test@1.2.3', entryModule); expect(pluginMap.size).toBe(1); - expect(overrideSharedModules).toHaveBeenCalledWith(entryModule); + expect(initSharedPluginModules).toHaveBeenCalledWith(entryModule); expect(resolveEncodedCodeRefs).not.toHaveBeenCalled(); expect(addDynamicPlugin).not.toHaveBeenCalled(); }); diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-loader.ts b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-loader.ts index 85f9593f3017..d151892ad4f0 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-loader.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/runtime/plugin-loader.ts @@ -5,7 +5,7 @@ import { PluginStore } from '@console/plugin-sdk/src/store'; import { resolveEncodedCodeRefs } from '../coderefs/coderef-resolver'; import { remoteEntryFile } from '../constants'; import { ConsolePluginManifestJSON } from '../schema/plugin-manifest'; -import { overrideSharedModules } from '../shared-modules-override'; +import { initSharedPluginModules } from '../shared-modules-init'; import { RemoteEntryModule } from '../types'; import { resolveURL } from '../utils/url'; import { fetchPluginManifest } from './plugin-manifest'; @@ -65,7 +65,7 @@ export const loadDynamicPlugin = (baseURL: string, manifest: ConsolePluginManife export const getPluginEntryCallback = ( pluginStore: PluginStore, - overrideSharedModulesCallback: typeof overrideSharedModules, + initSharedPluginModulesCallback: typeof initSharedPluginModules, resolveEncodedCodeRefsCallback: typeof resolveEncodedCodeRefs, ) => (pluginID: string, entryModule: RemoteEntryModule) => { if (!pluginMap.has(pluginID)) { @@ -83,9 +83,9 @@ export const getPluginEntryCallback = ( pluginData.entryCallbackFired = true; try { - overrideSharedModulesCallback(entryModule); + initSharedPluginModulesCallback(entryModule); } catch (error) { - console.error(`Failed to override shared modules for plugin ${pluginID}`, error); + console.error(`Failed to initialize shared modules for plugin ${pluginID}`, error); return; } @@ -105,7 +105,7 @@ export const getPluginEntryCallback = ( export const registerPluginEntryCallback = (pluginStore: PluginStore) => { window.loadPluginEntry = getPluginEntryCallback( pluginStore, - overrideSharedModules, + initSharedPluginModules, resolveEncodedCodeRefs, ); }; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-init.ts b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-init.ts new file mode 100644 index 000000000000..b8e68a66787b --- /dev/null +++ b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-init.ts @@ -0,0 +1,58 @@ +/* eslint-disable global-require */ +/* eslint-disable @typescript-eslint/no-require-imports */ + +import { SharedModuleResolution, RemoteEntryModule } from './types'; + +const modules: SharedModuleResolution = { + '@openshift-console/dynamic-plugin-sdk': async () => () => + require('@console/dynamic-plugin-sdk/src/lib-core'), + '@openshift-console/dynamic-plugin-sdk-internal': async () => () => + require('@console/dynamic-plugin-sdk/src/lib-internal'), + '@openshift-console/dynamic-plugin-sdk-internal-kubevirt': async () => () => + require('@console/dynamic-plugin-sdk/src/lib-internal-kubevirt'), + '@patternfly/react-core': async () => () => require('@patternfly/react-core'), + '@patternfly/react-table': async () => () => require('@patternfly/react-table'), + react: async () => () => require('react'), + 'react-helmet': async () => () => require('react-helmet'), + 'react-i18next': async () => () => require('react-i18next'), + 'react-router': async () => () => require('react-router'), + 'react-router-dom': async () => () => require('react-router-dom'), +}; + +const sharedScope = Object.keys(modules).reduce( + (acc, moduleRequest) => ({ + ...acc, + [moduleRequest]: { + // The '*' semver range means "this shared module matches all requested versions", + // i.e. make sure the plugin always uses the Console-provided shared module version + '*': { + get: modules[moduleRequest], + // Indicates that Console has already loaded the shared module + loaded: true, + }, + }, + }), + {}, +); + +/** + * At runtime, the Console application will initialize shared modules for each + * dynamic plugin before loading any of the modules exposed by the given plugin. + * + * Currently, module sharing is strictly unidirectional (Console -> plugins). + * + * Note: `__webpack_init_sharing__` global function is available in webpack 5+ builds. + * Once Console gets built with webpack 5, evaluate if we need this global in order to + * allow plugins to attempt to provide shared modules into the application shared scope. + * + * @see https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers + */ +export const initSharedPluginModules = (entryModule: RemoteEntryModule) => { + if (typeof entryModule.override === 'function') { + // Support plugins built with webpack 5.0.0-beta.16 + entryModule.override(modules); + return; + } + + entryModule.init(sharedScope); +}; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-override.ts b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-override.ts deleted file mode 100644 index 849c9e0ebb20..000000000000 --- a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules-override.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable global-require */ -/* eslint-disable @typescript-eslint/no-require-imports */ - -import { RemoteEntryModule } from './types'; - -/** - * At runtime, Console will override (i.e. enforce Console-bundled implementation of) shared - * modules for each dynamic plugin, before loading any of the modules exposed by that plugin. - * - * This way, a single version of React etc. is used by Console application and its plugins. - */ -const overrides = { - '@openshift-console/dynamic-plugin-sdk': async () => () => - require('@console/dynamic-plugin-sdk/src/lib-core'), - '@openshift-console/dynamic-plugin-sdk-internal': async () => () => - require('@console/dynamic-plugin-sdk/src/lib-internal'), - '@openshift-console/dynamic-plugin-sdk-internal-kubevirt': async () => () => - require('@console/dynamic-plugin-sdk/src/lib-internal-kubevirt'), - '@patternfly/react-core': async () => () => require('@patternfly/react-core'), - '@patternfly/react-table': async () => () => require('@patternfly/react-table'), - react: async () => () => require('react'), - 'react-helmet': async () => () => require('react-helmet'), - 'react-i18next': async () => () => require('react-i18next'), - 'react-router': async () => () => require('react-router'), - 'react-router-dom': async () => () => require('react-router-dom'), -}; - -export const overrideSharedModules = (entryModule: RemoteEntryModule) => { - if (entryModule.init) { - entryModule.init( - Object.keys(overrides).reduce((acc, override) => { - acc[override] = { - '*': { - get: overrides[override], - loaded: true, - }, - }; - return acc; - }, {}), - ); - } else { - entryModule.override(overrides); - } -}; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules.ts b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules.ts index 2044997e8ff4..bcf8b9fee2b9 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/shared-modules.ts @@ -1,35 +1,15 @@ /** * Modules shared between the Console application and its dynamic plugins. */ -export const sharedPluginModules = { - '@openshift-console/dynamic-plugin-sdk': { - singleton: true, - }, - '@openshift-console/dynamic-plugin-sdk-internal': { - singleton: true, - }, - '@openshift-console/dynamic-plugin-sdk-internal-kubevirt': { - singleton: true, - }, - '@patternfly/react-core': { - singleton: true, - }, - '@patternfly/react-table': { - singleton: true, - }, - react: { - singleton: true, - }, - 'react-helmet': { - singleton: true, - }, - 'react-i18next': { - singleton: true, - }, - 'react-router': { - singleton: true, - }, - 'react-router-dom': { - singleton: true, - }, -}; +export const sharedPluginModules = [ + '@openshift-console/dynamic-plugin-sdk', + '@openshift-console/dynamic-plugin-sdk-internal', + '@openshift-console/dynamic-plugin-sdk-internal-kubevirt', + '@patternfly/react-core', + '@patternfly/react-table', + 'react', + 'react-helmet', + 'react-i18next', + 'react-router', + 'react-router-dom', +]; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/types.ts b/frontend/packages/console-dynamic-plugin-sdk/src/types.ts index 1596a153d5e6..3618a42f5525 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/types.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/types.ts @@ -43,27 +43,32 @@ export type ExtensionDeclaration = Extension

}; /** - * Remote (i.e. webpack container) entry module interface. + * Resolution of modules shared between the Console application and its dynamic plugins. + */ +export type SharedModuleResolution = { [moduleRequest: string]: () => Promise<() => any> }; + +/** + * Remote webpack container entry module interface. */ export type RemoteEntryModule = { /** - * Get a module exposed through the container. - * - * Fails if the requested module doesn't exist in container. + * Initialize the container with modules provided via the shared scope. */ - get: (moduleName: string) => Promise<() => T>; - - init: (modules: any) => void; + init: (sharedScope: any) => void; /** - * For webpack 5.0.0-beta.16 + * _For webpack 5.0.0-beta.16 compatibility_ + * * Override module(s) that were flagged by the container as "overridable". + */ + override?: (modules: SharedModuleResolution) => void; + + /** + * Get a module exposed through the container. * - * All modules exposed through the container will use the given replacement modules - * instead of the container-local modules. If an override doesn't exist, all modules - * of the container will use the container-local module implementation. + * Fails if the requested module doesn't exist in the container. */ - override?: (modules: { [moduleName: string]: () => Promise<() => any> }) => void; + get: (moduleRequest: string) => Promise<() => T>; }; /** diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/utils/test-utils.ts b/frontend/packages/console-dynamic-plugin-sdk/src/utils/test-utils.ts index e7a70f590c1a..7b60021fd3d1 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/utils/test-utils.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/utils/test-utils.ts @@ -45,5 +45,6 @@ export type RemoteEntryModuleMock = Update< { get: jest.Mock>; init: jest.Mock; + override?: jest.Mock; } >; diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleAssetPlugin.ts b/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleAssetPlugin.ts index 3ea1e5f62597..e30be43a3493 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleAssetPlugin.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleAssetPlugin.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as findUp from 'find-up'; import * as webpack from 'webpack'; -import { extensionsFile, pluginManifestFile } from '../constants'; +import { extensionsFile, pluginManifestFile, remoteEntryFile } from '../constants'; import { ConsoleExtensionsJSON } from '../schema/console-extensions'; import { ConsolePluginManifestJSON } from '../schema/plugin-manifest'; import { ConsolePackageJSON } from '../schema/plugin-package'; @@ -29,62 +29,83 @@ export const validateExtensionsFileSchema = ( return new SchemaValidator(description).validate(schema, ext); }; -const emitJSON = (compilation: webpack.Compilation, filename: string, data: any) => { - const content = JSON.stringify(data, null, 2); - - // webpack compilation.emitAsset API requires the source argument to implement - // methods which aren't strictly needed for processing the asset. In this case, - // we just provide the content (source) and its length (size). - - // TODO(vojtech): revisit after bumping webpack 5 to latest stable version - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore - compilation.emitAsset(filename, { - source: () => content, - size: () => content.length, - }); -}; +const getPluginManifest = ( + pkg: ConsolePackageJSON, + ext: ConsoleExtensionsJSON, +): ConsolePluginManifestJSON => ({ + name: pkg.consolePlugin.name, + version: pkg.consolePlugin.version, + displayName: pkg.consolePlugin.displayName, + description: pkg.consolePlugin.description, + dependencies: pkg.consolePlugin.dependencies, + disableStaticPlugins: pkg.consolePlugin.disableStaticPlugins, + extensions: ext, +}); export class ConsoleAssetPlugin { - private readonly manifest: ConsolePluginManifestJSON; + private readonly ext: ConsoleExtensionsJSON; - constructor(private readonly pkg: ConsolePackageJSON) { - const ext = parseJSONC(path.resolve(process.cwd(), extensionsFile)); - validateExtensionsFileSchema(ext).report(); - - this.manifest = { - name: pkg.consolePlugin.name, - version: pkg.consolePlugin.version, - displayName: pkg.consolePlugin.displayName, - description: pkg.consolePlugin.description, - dependencies: pkg.consolePlugin.dependencies, - disableStaticPlugins: pkg.consolePlugin.disableStaticPlugins, - extensions: ext, - }; + constructor( + private readonly pkg: ConsolePackageJSON, + private readonly remoteEntryCallback: string, + private readonly skipExtensionValidator = false, + ) { + this.ext = parseJSONC(path.resolve(process.cwd(), extensionsFile)); + validateExtensionsFileSchema(this.ext).report(); } apply(compiler: webpack.Compiler) { - compiler.hooks.shouldEmit.tap(ConsoleAssetPlugin.name, (compilation) => { - const result = new ExtensionValidator(extensionsFile).validate( - compilation, - this.manifest.extensions, - this.pkg.consolePlugin.exposedModules || {}, + compiler.hooks.thisCompilation.tap(ConsoleAssetPlugin.name, (compilation) => { + // Generate additional assets + compilation.hooks.processAssets.tap( + { + name: ConsoleAssetPlugin.name, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + }, + () => { + compilation.emitAsset( + pluginManifestFile, + new webpack.sources.RawSource( + Buffer.from(JSON.stringify(getPluginManifest(this.pkg, this.ext), null, 2)), + ), + ); + }, ); - if (result.hasErrors()) { - result.getErrors().forEach((e) => { - // TODO(vojtech): revisit after bumping webpack 5 to latest stable version - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore - // @ts-ignore - compilation.errors.push(new Error(e)); - }); - return false; - } - return true; + // Post-process assets already present in the compilation + compilation.hooks.processAssets.tap( + { + name: ConsoleAssetPlugin.name, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + () => { + compilation.updateAsset(remoteEntryFile, (source) => { + const newSource = new webpack.sources.ReplaceSource(source); + newSource.insert( + this.remoteEntryCallback.length + 1, + `'${this.pkg.consolePlugin.name}@${this.pkg.consolePlugin.version}', `, + ); + return newSource; + }); + }, + ); }); - compiler.hooks.emit.tap(ConsoleAssetPlugin.name, (compilation) => { - emitJSON(compilation, pluginManifestFile, this.manifest); - }); + if (!this.skipExtensionValidator) { + compiler.hooks.emit.tap(ConsoleAssetPlugin.name, (compilation) => { + const result = new ExtensionValidator(extensionsFile).validate( + compilation, + this.ext, + this.pkg.consolePlugin.exposedModules || {}, + ); + + if (result.hasErrors()) { + const webpackError = new webpack.WebpackError('ExtensionValidator has reported errors'); + webpackError.details = result.formatErrors(); + webpackError.file = extensionsFile; + compilation.errors.push(webpackError); + } + }); + } } } diff --git a/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleRemotePlugin.ts b/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleRemotePlugin.ts index fd0eadda1722..5a28082861e2 100644 --- a/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleRemotePlugin.ts +++ b/frontend/packages/console-dynamic-plugin-sdk/src/webpack/ConsoleRemotePlugin.ts @@ -1,7 +1,6 @@ import * as _ from 'lodash'; import * as readPkg from 'read-pkg'; import * as webpack from 'webpack'; -import { ReplaceSource } from 'webpack-sources'; import { remoteEntryFile } from '../constants'; import { ConsolePackageJSON } from '../schema/plugin-package'; import { sharedPluginModules } from '../shared-modules'; @@ -36,9 +35,16 @@ export const validatePackageFileSchema = ( return validator.result; }; -const remoteEntryLibraryType = 'jsonp'; -const remoteEntryCallback = 'window.loadPluginEntry'; - +/** + * Generates Console dynamic plugin remote container and related assets. + * + * All modules shared between the Console application and its dynamic plugins are treated as singletons. + * Plugins won't bring their own fallback version of shared modules; Console is responsible for providing + * all shared modules to all of its plugins. + * + * If you're facing issues related to `ExtensionValidator`, pass `CONSOLE_PLUGIN_SKIP_EXT_VALIDATOR=true` + * env. variable to your webpack command. + */ export class ConsoleRemotePlugin { private readonly pkg: ConsolePackageJSON; @@ -48,63 +54,55 @@ export class ConsoleRemotePlugin { } apply(compiler: webpack.Compiler) { - if (!compiler.options.output.enabledLibraryTypes.includes(remoteEntryLibraryType)) { - compiler.options.output.enabledLibraryTypes.push(remoteEntryLibraryType); - } - - // Apply relevant webpack plugins - compiler.hooks.afterPlugins.tap(ConsoleRemotePlugin.name, () => { - new webpack.container.ContainerPlugin({ - name: this.pkg.consolePlugin.name, - library: { type: remoteEntryLibraryType, name: remoteEntryCallback }, - filename: remoteEntryFile, - exposes: this.pkg.consolePlugin.exposedModules || {}, - }).apply(compiler); - - new webpack.sharing.SharePlugin({ - shared: sharedPluginModules, - }).apply(compiler); + const logger = compiler.getInfrastructureLogger(ConsoleRemotePlugin.name); + const publicPath = `/api/plugins/${this.pkg.consolePlugin.name}/`; + const remoteEntryCallback = 'window.loadPluginEntry'; - // Generate additional Console plugin assets - new ConsoleAssetPlugin(this.pkg).apply(compiler); - - // Ignore require calls for modules that reside in Console monorepo packages - new webpack.IgnorePlugin({ - resourceRegExp: /^@console\//, - contextRegExp: /node_modules\/@openshift-console\/dynamic-plugin-sdk/, - }).apply(compiler); - }); + // Validate webpack options + if (compiler.options.output.publicPath !== undefined) { + logger.warn(`output.publicPath is defined, but will be overridden to ${publicPath}`); + } - // Post-process generated remote entry source - // TODO(vojtech): fix 'webpack-sources' type incompatibility when updating to latest webpack 5 - compiler.hooks.emit.tap(ConsoleRemotePlugin.name, (compilation) => { - compilation.updateAsset(remoteEntryFile, (source) => { - const newSource = new ReplaceSource(source as any); - newSource.insert( - remoteEntryCallback.length + 1, - `'${this.pkg.consolePlugin.name}@${this.pkg.consolePlugin.version}',`, - ); - return newSource; - }); + // Configure base path for loading all plugin assets + compiler.hooks.initialize.tap(ConsoleRemotePlugin.name, () => { + compiler.options.output.publicPath = publicPath; }); - // Skip processing entry option if it's missing or empty - // TODO(vojtech): latest webpack 5 allows `entry: {}` so use that & remove following code - if (_.isPlainObject(compiler.options.entry) && _.isEmpty(compiler.options.entry)) { - compiler.hooks.entryOption.tap(ConsoleRemotePlugin.name, () => { - return true; - }); - } - - // Set default publicPath if output.publicPath option is missing or empty - // TODO(vojtech): mainTemplate is deprecated in latest webpack 5, adapt code accordingly - if (_.isEmpty(compiler.options.output.publicPath)) { - compiler.hooks.thisCompilation.tap(ConsoleRemotePlugin.name, (compilation) => { - compilation.mainTemplate.hooks.requireExtensions.tap(ConsoleRemotePlugin.name, () => { - const pluginBaseURL = `/api/plugins/${this.pkg.consolePlugin.name}/`; - return `${webpack.RuntimeGlobals.publicPath} = "${pluginBaseURL}";`; - }); - }); - } + // Generate webpack federated module container assets + new webpack.container.ModuleFederationPlugin({ + name: this.pkg.consolePlugin.name, + library: { + type: 'jsonp', + name: remoteEntryCallback, + }, + filename: remoteEntryFile, + exposes: _.mapValues( + this.pkg.consolePlugin.exposedModules || {}, + (moduleRequest, moduleName) => ({ + import: moduleRequest, + name: `exposed-${moduleName}`, + }), + ), + shared: sharedPluginModules.reduce( + (acc, moduleRequest) => ({ + ...acc, + // https://webpack.js.org/plugins/module-federation-plugin/#sharing-hints + [moduleRequest]: { + // Allow only a single version of the shared module at runtime + singleton: true, + // Prevent plugins from using a fallback version of the shared module + import: false, + }, + }), + {}, + ), + }).apply(compiler); + + // Generate and/or post-process Console plugin assets + new ConsoleAssetPlugin( + this.pkg, + remoteEntryCallback, + process.env.CONSOLE_PLUGIN_SKIP_EXT_VALIDATOR === 'true', + ).apply(compiler); } } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index fd6e5a132289..df5599b3ce61 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -18019,7 +18019,7 @@ typescript@3.8.3, typescript@^3.6.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== -typescript@~4.2.3: +typescript@4.2.x, typescript@~4.2.3: version "4.2.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== @@ -19114,6 +19114,36 @@ webpack-virtual-modules@^0.4.3: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.3.tgz#cd597c6d51d5a5ecb473eea1983a58fa8a17ded9" integrity sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw== +webpack@5.x: + version "5.62.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.62.1.tgz#06f09b56a7b1bb13ed5137ad4b118358a90c9505" + integrity sha512-jNLtnWChS2CMZ7vqWtztv0G6fYB5hz11Zsadp5tE7e4/66zVDj7/KUeQZOsOl8Hz5KrLJH1h2eIDl6AnlyE12Q== + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.50" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.8.3" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.4" + json-parse-better-errors "^1.0.2" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.2.0" + webpack-sources "^3.2.0" + webpack@^4.46.0: version "4.46.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" @@ -19173,36 +19203,6 @@ webpack@^5.38.1: watchpack "^2.2.0" webpack-sources "^3.2.0" -webpack@^5.53.0: - version "5.61.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.61.0.tgz#fa827f0ee9bdfd141dd73c3e891e955ebd52fe7f" - integrity sha512-fPdTuaYZ/GMGFm4WrPi2KRCqS1vDp773kj9S0iI5Uc//5cszsFEDgHNaX4Rj1vobUiU1dFIV3mA9k1eHeluFpw== - dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.50" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.4.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.3" - es-module-lexer "^0.9.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.1.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.2.0" - webpack-sources "^3.2.0" - websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"