/** OpenShift Dynamic Plugin SDK https://github.com/openshift/dynamic-plugin-sdk packageName : @openshift/dynamic-plugin-sdk-webpack packageVersion : 3.0.1 buildDate : April 13, 2023 buildTime : 9:48:50 PM GMT+2 gitCommit : 7481e17e843e6e63966818dff104648c7e35a360 gitBranch : main */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var yup = require('yup'); var isEmpty = require('lodash/isEmpty'); var mapValues = require('lodash/mapValues'); var webpack = require('webpack'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var yup__namespace = /*#__PURE__*/_interopNamespace(yup); var isEmpty__default = /*#__PURE__*/_interopDefaultLegacy(isEmpty); var mapValues__default = /*#__PURE__*/_interopDefaultLegacy(mapValues); const DEFAULT_REMOTE_ENTRY_CALLBACK = '__load_plugin_entry__'; // TODO(vojtech): suppress false positive https://github.com/jsx-eslint/eslint-plugin-react/pull/3326 /** * Schema for a valid SemVer string. * * @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string */ const semverStringSchema = yup__namespace .string() .required() .matches(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/); /** * Schema for a valid plugin name. * * @example * ``` * foo * foo-bar * foo.bar * foo.bar-Test * Foo-Bar-abc.123 * ``` */ const pluginNameSchema = yup__namespace .string() .required() .matches(/^[a-zA-Z]+(?:[-.]?[a-zA-Z0-9]+)*$/); /** * Schema for a valid extension type. * * @example * ``` * app.foo * app.foo-bar * app.foo/bar * My-app.Foo-Bar * My-app.Foo-Bar/abcTest * ``` */ const extensionTypeSchema = yup__namespace .string() .required() .matches(/^[a-zA-Z]+(?:-[a-zA-Z]+)*\.[a-zA-Z]+(?:-[a-zA-Z]+)*(?:\/[a-zA-Z]+(?:-[a-zA-Z]+)*)*$/); /** * Schema for a valid feature flag name. * * @example * ``` * FOO * FOO_BAR * FOO_BAR123 * ``` */ const featureFlagNameSchema = yup__namespace .string() .required() .matches(/^[A-Z]+[A-Z0-9_]*$/); /** * Schema for `Extension` objects. */ const extensionSchema = yup__namespace .object() .required() .shape({ type: extensionTypeSchema, properties: yup__namespace.object().required(), flags: yup__namespace.object().shape({ required: yup__namespace.array().of(featureFlagNameSchema), disallowed: yup__namespace.array().of(featureFlagNameSchema), }), }); /** * Schema for an array of `Extension` objects. */ const extensionArraySchema = yup__namespace.array().of(extensionSchema).required(); /** * Schema for `PluginRegistrationMethod` objects. */ const pluginRegistrationMethodSchema = yup__namespace .mixed() .oneOf(['callback', 'custom']) .required(); /** * Schema for `PluginRuntimeMetadata` objects. */ const pluginRuntimeMetadataSchema = yup__namespace.object().required().shape({ name: pluginNameSchema, version: semverStringSchema, // TODO(vojtech): Yup lacks native support for map-like structures with arbitrary keys dependencies: yup__namespace.object(), customProperties: yup__namespace.object(), }); /** * Schema for `PluginManifest` objects. */ pluginRuntimeMetadataSchema.shape({ baseURL: yup__namespace.string().required(), extensions: extensionArraySchema, loadScripts: yup__namespace.array().of(yup__namespace.string().required()).required(), registrationMethod: pluginRegistrationMethodSchema, buildHash: yup__namespace.string(), }); /** * Schema for `PluginBuildMetadata` objects. */ const pluginBuildMetadataSchema = pluginRuntimeMetadataSchema.shape({ // TODO(vojtech): Yup lacks native support for map-like structures with arbitrary keys exposedModules: yup__namespace.object(), }); /** * Schema for `PluginModuleFederationSettings` objects. */ const pluginModuleFederationSettingsSchema = yup__namespace.object().required().shape({ libraryType: yup__namespace.string(), sharedScope: yup__namespace.string(), }); /** * Schema for `PluginEntryCallbackSettings` objects. */ const pluginEntryCallbackSettingsSchema = yup__namespace.object().required().shape({ name: yup__namespace.string(), pluginID: yup__namespace.string(), }); /** * Schema for adapted `DynamicRemotePluginOptions` objects. */ const dynamicRemotePluginAdaptedOptionsSchema = yup__namespace.object().required().shape({ pluginMetadata: pluginBuildMetadataSchema, extensions: extensionArraySchema, sharedModules: yup__namespace.object().required(), moduleFederationSettings: pluginModuleFederationSettingsSchema, entryCallbackSettings: pluginEntryCallbackSettingsSchema, entryScriptFilename: yup__namespace.string().required(), pluginManifestFilename: yup__namespace.string().required(), }); const findPluginChunks = (containerName, compilation) => { const allChunks = Array.from(compilation.chunks); const entryChunk = allChunks.find((chunk) => chunk.name === containerName); if (!entryChunk) { throw new Error(`Cannot find entry chunk ${containerName}`); } if (entryChunk.hasRuntime()) { return { entryChunk }; } const runtimeChunk = allChunks.find((chunk) => chunk.name === entryChunk.runtime); if (!runtimeChunk) { throw new Error(`Cannot find runtime chunk for entry chunk ${containerName}`); } return { entryChunk, runtimeChunk }; }; class GenerateManifestPlugin { constructor(containerName, manifestFilename, manifestData) { this.containerName = containerName; this.manifestFilename = manifestFilename; this.manifestData = manifestData; } apply(compiler) { const publicPath = compiler.options.output.publicPath; if (!publicPath) { throw new Error('output.publicPath option must be set to ensure plugin assets are loaded properly in the browser'); } compiler.hooks.thisCompilation.tap(GenerateManifestPlugin.name, (compilation) => { compilation.hooks.processAssets.tap({ name: GenerateManifestPlugin.name, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, }, () => { const { entryChunk, runtimeChunk } = findPluginChunks(this.containerName, compilation); const loadScripts = (runtimeChunk ? [runtimeChunk, entryChunk] : [entryChunk]).reduce((acc, chunk) => [...acc, ...chunk.files], []); const manifest = { ...this.manifestData, baseURL: compilation.getAssetPath(publicPath, {}), loadScripts, buildHash: compilation.fullHash, }; compilation.emitAsset(this.manifestFilename, new webpack.sources.RawSource(Buffer.from(JSON.stringify(manifest, null, 2)))); const warnings = []; if (manifest.extensions.length === 0) { warnings.push('Plugin has no extensions'); } if (!manifest.baseURL.endsWith('/')) { warnings.push('Plugin base URL (output.publicPath) should have a trailing slash'); } warnings.forEach((message) => { const error = new webpack.WebpackError(message); error.file = this.manifestFilename; compilation.warnings.push(error); }); }); }); } } class PatchEntryCallbackPlugin { constructor(containerName, callbackName, pluginID) { this.containerName = containerName; this.callbackName = callbackName; this.pluginID = pluginID; } apply(compiler) { compiler.hooks.thisCompilation.tap(PatchEntryCallbackPlugin.name, (compilation) => { compilation.hooks.processAssets.tap({ name: PatchEntryCallbackPlugin.name, stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, () => { const { entryChunk } = findPluginChunks(this.containerName, compilation); entryChunk.files.forEach((fileName) => { compilation.updateAsset(fileName, (source) => { const newSource = new webpack.sources.ReplaceSource(source); const fromIndex = source.source().toString().indexOf(`${this.callbackName}(`); if (fromIndex >= 0) { newSource.insert(fromIndex + this.callbackName.length + 1, `'${this.pluginID}', `); } else { const error = new webpack.WebpackError(`Missing call to ${this.callbackName}`); error.file = fileName; error.chunk = entryChunk; compilation.errors.push(error); } return newSource; }); }); }); }); } } class ValidateCompilationPlugin { constructor(containerName, jsonpLibraryType) { this.containerName = containerName; this.jsonpLibraryType = jsonpLibraryType; } apply(compiler) { compiler.hooks.done.tap(ValidateCompilationPlugin.name, ({ compilation }) => { const { runtimeChunk } = findPluginChunks(this.containerName, compilation); if (runtimeChunk) { const errorMessage = this.jsonpLibraryType ? 'Detected separate runtime chunk while using jsonp library type.\n' + 'This configuration is not allowed since it will cause issues when reloading plugins at runtime.\n' + 'Please update your webpack configuration to avoid emitting a separate runtime chunk.' : 'Detected separate runtime chunk while using non-jsonp library type.\n' + 'This configuration is not recommended since it may cause issues when reloading plugins at runtime.\n' + 'Consider updating your webpack configuration to avoid emitting a separate runtime chunk.'; const error = new webpack.WebpackError(errorMessage); error.chunk = runtimeChunk; (this.jsonpLibraryType ? compilation.errors : compilation.warnings).push(error); } }); } } const DEFAULT_MANIFEST = 'plugin-manifest.json'; const DEFAULT_ENTRY_SCRIPT = 'plugin-entry.js'; class DynamicRemotePlugin { constructor(options) { this.adaptedOptions = { pluginMetadata: options.pluginMetadata, extensions: options.extensions, sharedModules: options.sharedModules ?? {}, moduleFederationSettings: options.moduleFederationSettings ?? {}, entryCallbackSettings: options.entryCallbackSettings ?? {}, entryScriptFilename: options.entryScriptFilename ?? DEFAULT_ENTRY_SCRIPT, pluginManifestFilename: options.pluginManifestFilename ?? DEFAULT_MANIFEST, }; try { dynamicRemotePluginAdaptedOptionsSchema .strict(true) .validateSync(this.adaptedOptions, { abortEarly: false }); } catch (e) { throw new Error(`Invalid ${DynamicRemotePlugin.name} options:\n` + e.errors.join('\n')); } } apply(compiler) { const { pluginMetadata, extensions, sharedModules, moduleFederationSettings, entryCallbackSettings, entryScriptFilename, pluginManifestFilename, } = this.adaptedOptions; const containerName = pluginMetadata.name; const moduleFederationLibraryType = moduleFederationSettings.libraryType ?? 'jsonp'; const moduleFederationSharedScope = moduleFederationSettings.sharedScope ?? 'default'; const entryCallbackName = entryCallbackSettings.name ?? DEFAULT_REMOTE_ENTRY_CALLBACK; const entryCallbackPluginID = entryCallbackSettings.pluginID ?? pluginMetadata.name; const jsonp = moduleFederationLibraryType === 'jsonp'; const containerLibrary = { type: moduleFederationLibraryType, name: jsonp ? entryCallbackName : containerName, }; const containerModules = mapValues__default["default"](pluginMetadata.exposedModules ?? {}, (moduleRequest, moduleName) => ({ import: moduleRequest, name: `exposed-${moduleName}`, })); // Assign a unique name for the webpack build compiler.options.output.uniqueName ??= containerName; // Generate webpack federated module container assets new webpack.container.ModuleFederationPlugin({ name: containerName, library: containerLibrary, filename: entryScriptFilename, exposes: containerModules, shared: sharedModules, shareScope: moduleFederationSharedScope, }).apply(compiler); // ModuleFederationPlugin does not generate a container entry when the provided // exposes option is empty; we fix that by invoking the ContainerPlugin manually if (isEmpty__default["default"](containerModules)) { new webpack.container.ContainerPlugin({ name: containerName, library: containerLibrary, filename: entryScriptFilename, exposes: containerModules, shareScope: moduleFederationSharedScope, }).apply(compiler); } // Generate plugin manifest new GenerateManifestPlugin(containerName, pluginManifestFilename, { name: pluginMetadata.name, version: pluginMetadata.version, dependencies: pluginMetadata.dependencies, customProperties: pluginMetadata.customProperties, extensions, registrationMethod: jsonp ? 'callback' : 'custom', }).apply(compiler); // Post-process container entry generated by ModuleFederationPlugin if (jsonp) { new PatchEntryCallbackPlugin(containerName, entryCallbackName, entryCallbackPluginID).apply(compiler); } // Validate webpack compilation new ValidateCompilationPlugin(containerName, jsonp).apply(compiler); } } exports.DynamicRemotePlugin = DynamicRemotePlugin;