diff --git a/frontend/get-active-plugins.js b/frontend/get-active-plugins.js new file mode 100644 index 000000000000..2d96a305bb8a --- /dev/null +++ b/frontend/get-active-plugins.js @@ -0,0 +1,36 @@ +/** + * Get the current Console active plugins virtual module information. + * + * This module is executed by webpack `val-loader` which uses the resulting `code` as actual module source. + * + * + * @param {object} options + * @param {GetModuleData} options.getModuleData + * @param {import('webpack').LoaderContext} loaderContext + */ + +module.exports = ({ getModuleData }, loaderContext) => { + const { + code, + diagnostics: { errors, warnings }, + fileDependencies, + } = getModuleData(); + // eslint-disable-next-line no-console + console.log( + `Console active plugins virtual module code generated with ${errors.length} errors and ${warnings.length} warnings`, + ); + + errors.forEach((msg) => { + loaderContext.emitError(new Error(msg)); + }); + + warnings.forEach((msg) => { + loaderContext.emitWarning(new Error(msg)); + }); + + fileDependencies.forEach((file) => { + loaderContext.addDependency(file); + }); + + return { code }; +}; diff --git a/frontend/package.json b/frontend/package.json index 682a12ec8d17..d216fa28a67a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -337,7 +337,7 @@ "typescript": "4.5.5", "umd-compat-loader": "^2.1.2", "val-loader": "^6.0.0", - "webpack": "^5.75.0", + "webpack": "^5.95.0", "webpack-bundle-analyzer": "4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.1.0" diff --git a/frontend/packages/console-plugin-sdk/src/codegen/active-plugins.ts b/frontend/packages/console-plugin-sdk/src/codegen/active-plugins.ts index b3ba601d5580..b5bad5311025 100644 --- a/frontend/packages/console-plugin-sdk/src/codegen/active-plugins.ts +++ b/frontend/packages/console-plugin-sdk/src/codegen/active-plugins.ts @@ -1,9 +1,11 @@ import * as fs from 'fs'; +import * as path from 'path'; import * as _ from 'lodash'; import { isEncodedCodeRef, parseEncodedCodeRefValue, } from '@console/dynamic-plugin-sdk/src/coderefs/coderef-resolver'; +import { extensionsFile } from '@console/dynamic-plugin-sdk/src/constants'; import { ConsoleExtensionsJSON } from '@console/dynamic-plugin-sdk/src/schema/console-extensions'; import { EncodedCodeRef } from '@console/dynamic-plugin-sdk/src/types'; import { parseJSONC } from '@console/dynamic-plugin-sdk/src/utils/jsonc'; @@ -13,8 +15,44 @@ import { Extension, ActivePlugin } from '../typings'; import { trimStartMultiLine } from '../utils/string'; import { consolePkgScope, PluginPackage } from './plugin-resolver'; +const getExtensionsFilePath = (pkg: PluginPackage) => path.resolve(pkg._path, extensionsFile); + +const guessFileExtension = (basePath: string) => { + const extensions = ['.tsx', '.ts', '.js', '.jsx']; + + if (fs.existsSync(basePath)) { + return basePath; + } + + for (const ext of extensions) { + if (fs.existsSync(`${basePath}${ext}`)) { + return `${basePath}${ext}`; + } + } + + return basePath; +}; + +const getExposedModuleFilePath = (pkg: PluginPackage, moduleName: string) => { + const modulePath = path.resolve(pkg._path, pkg.consolePlugin.exposedModules[moduleName]); + + // Sometimes the module is an index.ts file, but the import path only + // specifies the directory. Thus we need an explicit check for the index file. + const indexPath = path.resolve(modulePath, 'index.ts'); + return fs.existsSync(indexPath) ? indexPath : guessFileExtension(modulePath); +}; + +export type ActivePluginsModuleData = { + /** Generated module source code. */ + code: string; + /** Diagnostics collected while generating module source code. */ + diagnostics: { errors: string[]; warnings: string[] }; + /** Absolute file paths representing webpack file dependencies of the generated module. */ + fileDependencies: string[]; +}; + /** - * Generate the `@console/active-plugins` virtual module source. + * Generate the Console active plugins virtual module source. */ export const getActivePluginsModule = ( pluginPackages: PluginPackage[], @@ -149,3 +187,45 @@ export const getDynamicExtensions = ( return trimStartMultiLine(source); }; + +export const getActivePluginsModuleData = ( + pluginPackages: PluginPackage[], +): ActivePluginsModuleData => { + const errors: string[] = []; + const warnings: string[] = []; + const fileDependencies: string[] = []; + + const code = getActivePluginsModule( + pluginPackages, + () => ` + import { applyCodeRefSymbol } from '@console/dynamic-plugin-sdk/src/coderefs/coderef-resolver'; + `, + (pkg) => + getDynamicExtensions( + pkg, + getExtensionsFilePath(pkg), + (errorMessage) => { + errors.push(errorMessage); + }, + (codeRefSource) => `applyCodeRefSymbol(${codeRefSource})`, + ), + ); + + for (const pkg of pluginPackages) { + fileDependencies.push(getExtensionsFilePath(pkg)); + + Object.keys(pkg.consolePlugin.exposedModules || {}).forEach((moduleName) => { + const moduleFilePath = getExposedModuleFilePath(pkg, moduleName); + + if (fs.existsSync(moduleFilePath) && fs.statSync(moduleFilePath).isFile()) { + fileDependencies.push(moduleFilePath); + } else { + warnings.push( + `Exposed module '${moduleName}' in static plugin ${pkg.name} refers to non-existent file ${moduleFilePath}`, + ); + } + }); + } + + return { code, diagnostics: { errors, warnings }, fileDependencies }; +}; diff --git a/frontend/packages/console-plugin-sdk/src/webpack/ConsoleActivePluginsModule.ts b/frontend/packages/console-plugin-sdk/src/webpack/ConsoleActivePluginsModule.ts deleted file mode 100644 index 0497795fb269..000000000000 --- a/frontend/packages/console-plugin-sdk/src/webpack/ConsoleActivePluginsModule.ts +++ /dev/null @@ -1,99 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as webpack from 'webpack'; -import { extensionsFile } from '@console/dynamic-plugin-sdk/src/constants'; -import { getActivePluginsModule, getDynamicExtensions } from '../codegen/active-plugins'; -import { PluginPackage } from '../codegen/plugin-resolver'; - -type VirtualModulesPluginAPI = { - writeModule: (filePath: string, source: string) => void; -}; - -const getExtensionsFilePath = (pkg: PluginPackage) => path.resolve(pkg._path, extensionsFile); - -const getPluginFiles = (pkg: PluginPackage) => { - const files = new Set(); - files.add(getExtensionsFilePath(pkg)); - - Object.keys(pkg.consolePlugin.exposedModules || {}).forEach((moduleName) => { - files.add(path.resolve(pkg._path, pkg.consolePlugin.exposedModules[moduleName])); - }); - - return files; -}; - -const getFileLastModified = (f: string) => (fs.existsSync(f) ? fs.statSync(f).mtimeMs : -1); - -export class ConsoleActivePluginsModule { - constructor( - private readonly pluginPackages: PluginPackage[], - private readonly virtualModules: VirtualModulesPluginAPI, - ) {} - - apply(compiler: webpack.Compiler) { - const lastModified = new Map(); - let errors: string[] = []; - - const checkFilesModified = () => { - let filesModified = false; - - this.pluginPackages.forEach((pkg) => { - getPluginFiles(pkg).forEach((f) => { - const mtime = getFileLastModified(f); - filesModified = filesModified || mtime !== lastModified.get(f); - lastModified.set(f, mtime); - }); - }); - - return filesModified; - }; - - const writeModule = () => { - if (checkFilesModified()) { - errors = []; - - this.virtualModules.writeModule( - 'node_modules/@console/active-plugins.js', - getActivePluginsModule( - this.pluginPackages, - () => ` - import { applyCodeRefSymbol } from '@console/dynamic-plugin-sdk/src/coderefs/coderef-resolver'; - `, - (pkg) => - getDynamicExtensions( - pkg, - getExtensionsFilePath(pkg), - (errorMessage) => { - errors.push(errorMessage); - }, - (codeRefSource) => `applyCodeRefSymbol(${codeRefSource})`, - ), - ), - ); - } - }; - - const addFilesToCompilation = (compilation: webpack.Compilation) => { - this.pluginPackages.forEach((pkg) => { - getPluginFiles(pkg).forEach((f) => { - compilation.fileDependencies.add(f); - }); - }); - }; - - const addErrorsToCompilation = (compilation: webpack.Compilation) => { - errors.forEach((e) => { - compilation.errors.push(new webpack.WebpackError(e)); - }); - }; - - compiler.hooks.afterResolvers.tap(ConsoleActivePluginsModule.name, writeModule); - compiler.hooks.watchRun.tap(ConsoleActivePluginsModule.name, writeModule); - compiler.hooks.afterCompile.tap(ConsoleActivePluginsModule.name, addFilesToCompilation); - - compiler.hooks.shouldEmit.tap(ConsoleActivePluginsModule.name, (compilation) => { - addErrorsToCompilation(compilation); - return errors.length === 0; - }); - } -} diff --git a/frontend/public/plugins.ts b/frontend/public/plugins.ts index 44a0946fd551..edb08caded2e 100644 --- a/frontend/public/plugins.ts +++ b/frontend/public/plugins.ts @@ -22,10 +22,11 @@ const getI18nNamespaces = () => { }; // The '@console/active-plugins' module is generated during a webpack build, +// Console active plugins module has its source generated during webpack build, // so we use dynamic require() instead of the usual static import statement. const activePlugins = process.env.NODE_ENV !== 'test' - ? (require('@console/active-plugins').default as ActivePlugin[]) + ? (require('../get-active-plugins').default as ActivePlugin[]) : []; const dynamicPluginNames = getEnabledDynamicPluginNames(); diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index 25de0585cfa6..e0108d16dd19 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -10,8 +10,8 @@ import * as glob from 'glob'; import { HtmlWebpackSkipAssetsPlugin } from 'html-webpack-skip-assets-plugin'; import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; import { sharedPluginModules } from '@console/dynamic-plugin-sdk/src/shared-modules'; +import { getActivePluginsModuleData } from '@console/plugin-sdk/src/codegen/active-plugins'; import { resolvePluginPackages } from '@console/plugin-sdk/src/codegen/plugin-resolver'; -import { ConsoleActivePluginsModule } from '@console/plugin-sdk/src/webpack/ConsoleActivePluginsModule'; import { CircularDependencyPreset } from './webpack.circular-deps'; const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); @@ -23,7 +23,6 @@ interface Configuration extends webpack.Configuration { const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -const VirtualModulesPlugin = require('webpack-virtual-modules'); const NODE_ENV = process.env.NODE_ENV || 'development'; const HOT_RELOAD = process.env.HOT_RELOAD || 'true'; @@ -34,13 +33,13 @@ const OPENSHIFT_CI = process.env.OPENSHIFT_CI; const WDS_PORT = 8080; /* Helpers */ +const staticPluginPackages = resolvePluginPackages(); const extractCSS = new MiniCssExtractPlugin({ filename: 'app-bundle.[name].[contenthash].css', // We follow BEM naming to scope CSS. // See https://github.com/webpack-contrib/mini-css-extract-plugin/issues/250 ignoreOrder: true, }); -const virtualModules = new VirtualModulesPlugin(); const getVendorModuleRegExp = (vendorModules: string[]) => new RegExp(`node_modules\\/(${vendorModules.map(_.escapeRegExp).join('|')})\\/`); @@ -135,6 +134,13 @@ const config: Configuration = { test: sharedPluginModulesTest, sideEffects: true, }, + { + test: path.resolve(__dirname, 'get-active-plugins.js'), + loader: 'val-loader', + options: { + getModuleData: () => getActivePluginsModuleData(staticPluginPackages), + }, + }, { test: /\.glsl$/, use: ['raw-loader', 'glslify-loader'] }, { test: /\.js$/, @@ -363,8 +369,6 @@ const config: Configuration = { ], }), extractCSS, - virtualModules, - new ConsoleActivePluginsModule(resolvePluginPackages(), virtualModules), ...(REACT_REFRESH ? [ new ReactRefreshWebpackPlugin({ diff --git a/frontend/yarn.lock b/frontend/yarn.lock index bd30c70c0fa5..0ce5357be2a2 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -17063,6 +17063,11 @@ v8-to-istanbul@^9.0.0: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" +val-loader@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/val-loader/-/val-loader-6.0.0.tgz#88078512fdb66e04deab659fec05e281105e4d81" + integrity sha512-NHi81ow+/mVBRuFRNxp8tfTSnAIFsq/wzZGqxv/a82Y722GQSOQi9yP0GuenSBiuw4+zGjmW/H9sLTbP3bewrw== + valid-url@1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" @@ -17729,11 +17734,6 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack-virtual-modules@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" - integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== - webpack@5.75.0: version "5.75.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"