@openshift-console/dynamic-plugin-sdk-webpack
Version:
Provides webpack ConsoleRemotePlugin used to build all dynamic plugin assets.
234 lines (233 loc) • 12 kB
JavaScript
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;
;