UNPKG

@openshift-console/dynamic-plugin-sdk-webpack

Version:

Provides webpack ConsoleRemotePlugin used to build all dynamic plugin assets.

234 lines (233 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConsoleRemotePlugin = exports.validateConsoleExtensionsFileSchema = void 0; const fs = require("fs"); const path = require("path"); const dynamic_plugin_sdk_webpack_1 = require("@openshift/dynamic-plugin-sdk-webpack"); const glob = require("glob"); const _ = require("lodash"); const readPkg = require("read-pkg"); const semver = require("semver"); const webpack = require("webpack"); const constants_1 = require("../constants"); const shared_modules_1 = require("../shared-modules"); const dynamic_module_parser_1 = require("../utils/dynamic-module-parser"); const jsonc_1 = require("../utils/jsonc"); const schema_1 = require("../utils/schema"); const ExtensionValidator_1 = require("../validation/ExtensionValidator"); const SchemaValidator_1 = require("../validation/SchemaValidator"); const ValidationResult_1 = require("../validation/ValidationResult"); const dynamicModuleImportLoader = '@openshift-console/dynamic-plugin-sdk-webpack/lib/webpack/loaders/dynamic-module-import-loader'; const loadPluginPackageJSON = () => readPkg.sync({ normalize: false }); const loadVendorPackageJSON = (moduleName) => // eslint-disable-next-line @typescript-eslint/no-var-requires require(`${moduleName}/package.json`); const getVendorPackageVersion = (moduleName) => { try { return loadVendorPackageJSON(moduleName).version; } catch (e) { return undefined; } }; const getPackageDependencies = (pkg) => ({ ...pkg.devDependencies, ...pkg.dependencies, }); const getPluginSDKPackageDependencies = () => loadVendorPackageJSON('@openshift-console/dynamic-plugin-sdk').dependencies; const getPatternFlyStyles = (baseDir) => glob.sync(`${baseDir}/node_modules/@patternfly/react-styles/**/*.css`); // https://webpack.js.org/plugins/module-federation-plugin/#sharing-hints const getWebpackSharedModules = () => { const sdkPkgDeps = getPluginSDKPackageDependencies(); return shared_modules_1.sharedPluginModules.reduce((acc, moduleName) => { const { singleton, allowFallback } = (0, shared_modules_1.getSharedModuleMetadata)(moduleName); const providedVersionRange = sdkPkgDeps[moduleName]; const moduleConfig = { singleton }; if (!allowFallback) { moduleConfig.import = false; } if (semver.validRange(providedVersionRange)) { moduleConfig.requiredVersion = providedVersionRange; } acc[moduleName] = moduleConfig; return acc; }, {}); }; const getWebpackSharedDynamicModules = (pkg, moduleName, moduleRequests) => { const pluginDeps = getPackageDependencies(pkg); const moduleVersion = getVendorPackageVersion(moduleName); const moduleVersionRange = pluginDeps[moduleName]; const moduleConfig = {}; if (semver.valid(moduleVersion)) { moduleConfig.version = moduleVersion; } if (semver.validRange(moduleVersionRange)) { moduleConfig.requiredVersion = moduleVersionRange; } return moduleRequests.reduce((acc, request) => { acc[`${moduleName}/${request}`] = moduleConfig; return acc; }, {}); }; /** * Perform (additional) build-time validation of Console plugin metadata. * * Note that `DynamicRemotePlugin` takes care of basic build metadata validation. * Therefore, this function only performs additional Console specific validations. */ const validateConsoleBuildMetadata = (metadata) => { const result = new ValidationResult_1.ValidationResult('Console plugin metadata'); result.assertions.validDNSSubdomainName(metadata.name, 'metadata.name'); return result; }; const validateConsoleExtensionsFileSchema = (extensions, description = 'console-extensions.json') => { const schema = (0, schema_1.loadSchema)('console-extensions.json'); return new SchemaValidator_1.SchemaValidator(description).validate(schema, extensions); }; exports.validateConsoleExtensionsFileSchema = validateConsoleExtensionsFileSchema; const validateConsoleProvidedSharedModules = (pkg) => { const pluginDeps = getPackageDependencies(pkg); const sdkPkgDeps = getPluginSDKPackageDependencies(); const result = new ValidationResult_1.ValidationResult('package.json'); shared_modules_1.sharedPluginModules.forEach((moduleName) => { const { allowFallback } = (0, shared_modules_1.getSharedModuleMetadata)(moduleName); // Skip modules that allow a fallback version to be provided by the plugin. // Also skip modules which are not explicitly listed in the plugin's dependencies. if (allowFallback || !pluginDeps[moduleName]) { return; } const providedVersionRange = sdkPkgDeps[moduleName]; const consumedVersion = getVendorPackageVersion(moduleName); if (semver.validRange(providedVersionRange) && semver.valid(consumedVersion)) { result.assertThat(semver.satisfies(consumedVersion, providedVersionRange), `Console provides shared module ${moduleName} ${providedVersionRange} but plugin uses version ${consumedVersion}`); } }); return result; }; /** * Generates Console dynamic plugin remote container and related assets. * * Refer to `frontend/packages/console-dynamic-plugin-sdk/src/shared-modules.ts` for details on * Console application vs. dynamic plugins shared module configuration. * * @see {@link sharedPluginModules} * @see {@link getSharedModuleMetadata} */ class ConsoleRemotePlugin { constructor(options = {}) { this.baseDir = process.cwd(); this.pkg = loadPluginPackageJSON(); this.adaptedOptions = { pluginMetadata: options.pluginMetadata ?? this.pkg.consolePlugin, extensions: options.extensions ?? (0, jsonc_1.parseJSONC)(path.resolve(this.baseDir, constants_1.extensionsFile)), validateExtensionSchema: options.validateExtensionSchema ?? true, validateExtensionIntegrity: options.validateExtensionIntegrity ?? true, validateSharedModules: options.validateSharedModules ?? true, sharedDynamicModuleSettings: options.sharedDynamicModuleSettings ?? {}, }; if (this.adaptedOptions.validateExtensionSchema) { (0, exports.validateConsoleExtensionsFileSchema)(this.adaptedOptions.extensions).report(); } if (this.adaptedOptions.validateSharedModules) { validateConsoleProvidedSharedModules(this.pkg).report(); } const resolvedModulePaths = this.adaptedOptions.sharedDynamicModuleSettings.modulePaths ?? [ path.resolve(process.cwd(), 'node_modules'), ]; this.sharedDynamicModuleMaps = Object.entries(this.adaptedOptions.sharedDynamicModuleSettings.packageSpecs ?? { '@patternfly/react-core': {}, '@patternfly/react-icons': {}, '@patternfly/react-table': {}, }).reduce((acc, [pkgName, { indexModule = 'dist/esm/index.js', resolutionField = 'module' }]) => { const basePath = resolvedModulePaths .map((p) => path.resolve(p, pkgName)) .find((p) => fs.existsSync(p) && fs.statSync(p).isDirectory()); return basePath ? { ...acc, [pkgName]: (0, dynamic_module_parser_1.getDynamicModuleMap)(basePath, indexModule, resolutionField) } : acc; }, {}); } apply(compiler) { const { pluginMetadata, extensions, validateExtensionIntegrity, sharedDynamicModuleSettings, } = this.adaptedOptions; const { name, version, dependencies, customProperties, exposedModules, displayName, description, disableStaticPlugins, } = pluginMetadata; const logger = compiler.getInfrastructureLogger(ConsoleRemotePlugin.name); const publicPath = `/api/plugins/${name}/`; if (compiler.options.output.publicPath !== undefined) { logger.warn(`output.publicPath is defined, but will be overridden to ${publicPath}`); } compiler.options.output.publicPath = publicPath; compiler.options.resolve = compiler.options.resolve ?? {}; compiler.options.resolve.alias = compiler.options.resolve.alias ?? {}; // Prevent PatternFly styles from being included in the compilation getPatternFlyStyles(this.baseDir).forEach((cssFile) => { if (Array.isArray(compiler.options.resolve.alias)) { compiler.options.resolve.alias.push({ name: cssFile, alias: false }); } else { compiler.options.resolve.alias[cssFile] = false; } }); const consoleProvidedSharedModules = getWebpackSharedModules(); const sharedDynamicModules = Object.entries(this.sharedDynamicModuleMaps).reduce((acc, [moduleName, dynamicModuleMap]) => ({ ...acc, ...getWebpackSharedDynamicModules(this.pkg, moduleName, Object.values(dynamicModuleMap)), }), {}); new dynamic_plugin_sdk_webpack_1.DynamicRemotePlugin({ pluginMetadata: { name, version, dependencies, customProperties: _.merge({}, customProperties, { console: { displayName, description, disableStaticPlugins }, }), exposedModules, }, extensions, sharedModules: { ...consoleProvidedSharedModules, ...sharedDynamicModules }, entryCallbackSettings: { name: 'loadPluginEntry', pluginID: `${name}@${version}`, }, entryScriptFilename: process.env.NODE_ENV === 'production' ? 'plugin-entry.[fullhash].min.js' : 'plugin-entry.js', }).apply(compiler); validateConsoleBuildMetadata(pluginMetadata).report(); if (validateExtensionIntegrity) { compiler.hooks.emit.tap(ConsoleRemotePlugin.name, (compilation) => { const result = new ExtensionValidator_1.ExtensionValidator('Console plugin extensions').validate(compilation, extensions, exposedModules ?? {}); if (result.hasErrors()) { const error = new webpack.WebpackError('ExtensionValidator has reported errors'); error.details = result.formatErrors(); error.file = constants_1.extensionsFile; compilation.errors.push(error); } }); } const transformImports = sharedDynamicModuleSettings.transformImports ?? ((moduleRequest) => { const isCode = /\.(jsx?|tsx?)$/.test(moduleRequest); const isVendor = moduleRequest.includes('/node_modules/'); return isCode && (!isVendor || moduleRequest.includes('/node_modules/@openshift-console/')); }); compiler.hooks.thisCompilation.tap(ConsoleRemotePlugin.name, (compilation) => { const modifiedModules = []; webpack.NormalModule.getCompilationHooks(compilation).beforeLoaders.tap(ConsoleRemotePlugin.name, (loaders, normalModule) => { const { userRequest } = normalModule; const moduleRequest = userRequest.substring(userRequest.lastIndexOf('!') === -1 ? 0 : userRequest.lastIndexOf('!') + 1); if (!modifiedModules.includes(moduleRequest) && transformImports(moduleRequest)) { const loaderOptions = { dynamicModuleMaps: this.sharedDynamicModuleMaps, resourceMetadata: { jsx: /\.(jsx|tsx)$/.test(moduleRequest) }, }; normalModule.loaders.push({ loader: dynamicModuleImportLoader, options: loaderOptions, }); modifiedModules.push(moduleRequest); } }); }); } } exports.ConsoleRemotePlugin = ConsoleRemotePlugin;