From ecf75860394c633aefaaf99ca3377cf7235b2432 Mon Sep 17 00:00:00 2001 From: MariaAga Date: Thu, 25 Jan 2024 13:15:10 +0100 Subject: [PATCH] webpack 5 - test no css import fix --- Gemfile | 1 - Procfile | 6 +- app/assets/config/manifest.js | 1 + app/assets/javascripts/application.js | 16 +- app/assets/javascripts/late_load.js | 53 +++ app/controllers/application_controller.rb | 16 - app/helpers/application_helper.rb | 5 - app/helpers/layout_helper.rb | 23 + app/helpers/reactjs_helper.rb | 47 +- app/services/foreman/env_settings_loader.rb | 2 - app/views/layouts/base.html.erb | 18 +- config/environments/development.rb | 4 - config/environments/production.rb | 2 - config/environments/test.rb | 2 - config/initializers/assets.rb | 40 -- config/settings.yaml.example | 9 - config/webpack.config.js | 424 ++++++++++-------- developer_docs/getting-started.asciidoc | 40 +- lib/tasks/jenkins.rake | 2 +- lib/tasks/plugin_assets.rake | 5 +- lib/tasks/webpack_compile.rake | 12 +- package-exclude.json | 2 - package.json | 33 +- script/foreman-start-dev | 2 +- test/helpers/reactjs_helper_test.rb | 6 +- test/integration/middleware_test.rb | 44 -- test/test_helper.rb | 15 +- test/unit/foreman/env_settings_loader_test.rb | 4 - webpack/assets/javascripts/foreman_tools.js | 19 + .../react_app/common/AwaitedMount.js | 60 +++ .../react_app/common/MountingService.js | 14 +- .../ForemanModal/ForemanModalContext.js | 3 +- .../HostDetails/ActionsBar/index.js | 7 +- .../HostDetails/CardExpansionContext.js | 5 +- webpack/simple_named_modules.js | 35 -- 35 files changed, 496 insertions(+), 481 deletions(-) create mode 100644 app/assets/javascripts/late_load.js delete mode 100644 test/integration/middleware_test.rb create mode 100644 webpack/assets/javascripts/react_app/common/AwaitedMount.js delete mode 100644 webpack/simple_named_modules.js diff --git a/Gemfile b/Gemfile index 344131cc4d22..6a7acce2e474 100644 --- a/Gemfile +++ b/Gemfile @@ -37,7 +37,6 @@ gem 'sprockets-rails', '~> 3.0' gem 'responders', '~> 3.0' gem 'roadie-rails', '~> 3.0' gem 'deacon', '~> 1.0' -gem 'webpack-rails', '~> 0.9.8' gem 'mail', '~> 2.7' gem 'sshkey', '~> 2.0' gem 'dynflow', '>= 1.6.5', '< 2.0.0' diff --git a/Procfile b/Procfile index f1791e16f5ef..0922df8cf66e 100644 --- a/Procfile +++ b/Procfile @@ -1,5 +1,7 @@ # Run Rails & Webpack concurrently # If you wish to use a different server then the default, use e.g. `export RAILS_STARTUP='puma -w 3 -p 3000 --preload'` rails: [ -n "$RAILS_STARTUP" ] && env PRY_WARNING=1 $RAILS_STARTUP || [ -n "$BIND" ] && bin/rails server -b $BIND || env PRY_WARNING=1 bin/rails server -# you can use WEBPACK_OPTS to customize webpack server, e.g. 'WEBPACK_OPTS='--https --key /path/to/key --cert /path/to/cert.pem --cacert /path/to/cacert.pem' foreman start ' -webpack: [ -n "$NODE_ENV" ] && ./node_modules/.bin/webpack-dev-server-without-h2 --config config/webpack.config.js $WEBPACK_OPTS || env NODE_ENV=development ./node_modules/.bin/webpack-dev-server-without-h2 --config config/webpack.config.js $WEBPACK_OPTS + +# you can use WEBPACK_OPTS to customize webpack server, e.g. 'WEBPACK_OPTS=--progress' foreman start ' +# filter out webpack options that are commonly used but not supported by webpack 5 and not needed in the new configutation as webpack is not run as a server anymore +webpack: FILTERED_WEBPACK_OPTS=$(echo $WEBPACK_OPTS | sed -e 's/--key [^ ]*//g' -e 's/--public [^ ]*//g' -e 's/--https [^ ]*//g' -e 's/--cert [^ ]*//g' -e 's/--cacert [^ ]*//g') && [ -n "$NODE_ENV" ] && npx webpack --config config/webpack.config.js --watch $FILTERED_WEBPACK_OPTS || env NODE_ENV=development npx webpack --config config/webpack.config.js --watch $FILTERED_WEBPACK_OPTS \ No newline at end of file diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index e06b86a6f2f9..91678641616e 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,4 @@ +//= link late_load.js //= link_tree ../../../vendor/assets/fonts //= link_tree ../images //= link application.css diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f08473375e4c..a055c1f19079 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,9 +7,22 @@ //= require lookup_keys $(function() { - $(document).trigger('ContentLoad'); + if(window.allJsLoaded){ + $(document).trigger('ContentLoad'); + } + else { + $(document).on('loadJS', function() { + $(document).trigger('ContentLoad'); + });} }); + +// Override jQuery's ready function to run only after all scripts are loaded instead of when the DOM is ready +$.fn.ready = function(fn) { + this.on('loadJS', fn); + return this; +}; + // Prevents all links with the disabled attribute set to "disabled" // from being clicked. var handleDisabledClick = function(event, element){ @@ -21,7 +34,6 @@ var handleDisabledClick = function(event, element){ $(document).on('click', 'a[disabled="disabled"]', function(event) { return handleDisabledClick(event, this); }); - function onContentLoad() { if ($('input[focus_on_load=true]').length > 0) { $('input[focus_on_load]') diff --git a/app/assets/javascripts/late_load.js b/app/assets/javascripts/late_load.js new file mode 100644 index 000000000000..55a0a4f3d540 --- /dev/null +++ b/app/assets/javascripts/late_load.js @@ -0,0 +1,53 @@ +function load_dynamic_javascripts(html) { + function waitForAllLoaded() { + // Wait for all plugins js modules to be loaded before loading the javascript content + return new Promise(function(resolve) { + // window.allPluginsLoaded is set to {} when plugins are starting to load + // if there are no plugins window.allPluginsLoaded is never defined + if (window.allPluginsLoaded === undefined || Object.values(window.allPluginsLoaded).every(Boolean)) { + resolve(); + } else { + function handleLoad() { + if (window.allPluginsLoaded === undefined || Object.values(window.allPluginsLoaded).every(Boolean)) { + resolve(); + // Remove the event listener + document.removeEventListener('loadPlugin', handleLoad); + } + } + document.addEventListener('loadPlugin', handleLoad); + } + }); + } + waitForAllLoaded().then(async function() { + // parse html string + var template = document.createElement('template'); + template.innerHTML = html; + var doc = new DOMParser().parseFromString(html, 'text/html'); + var copyChildren = [...doc.head.children]; + const loadScript = async scripts => { + if (scripts.length === 0) { + // All scripts are loaded + window.allJsLoaded = true; + const loadJS = new Event('loadJS'); + document.dispatchEvent(loadJS); + return; + } + const script = scripts.shift(); + if (script.src) { + // if script is just a link, add it to the head + const scriptTag = document.createElement('script'); + scriptTag.src = script.src; + scriptTag.onload = function() { + // To load the next script only after the current one is loaded + loadScript(scripts); + }; + document.head.appendChild(scriptTag); + } else { + // if the script is a script tag, evaluate it and load the next one + await eval(script.innerHTML); + loadScript(scripts); + } + }; + loadScript(copyChildren); + }); +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d1bc1dcdaddb..9321bf5811d6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -23,7 +23,6 @@ class ApplicationController < ActionController::Base before_action :set_taxonomy, :require_mail, :check_empty_taxonomy before_action :authorize before_action :welcome, :find_selected_columns, :only => :index, :unless => :api_request? - prepend_before_action :allow_webpack, if: -> { Rails.configuration.webpack.dev_server.enabled } around_action :set_timezone attr_reader :original_search_parameter @@ -399,21 +398,6 @@ def parameter_filter_context Foreman::ParameterFilter::Context.new(:ui, controller_name, params[:action]) end - def allow_webpack - webpack_csp = { - script_src: [webpack_server], connect_src: [webpack_server], - style_src: [webpack_server], img_src: [webpack_server], - font_src: ["data: #{webpack_server}"], default_src: [webpack_server] - } - - append_content_security_policy_directives(webpack_csp) - end - - def webpack_server - port = Rails.configuration.webpack.dev_server.port - @dev_server ||= "#{request.protocol}#{request.host}:#{port}" - end - class << self def parameter_filter_context Foreman::ParameterFilter::Context.new(:ui, controller_name, nil) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 98aa2cc2c85f..d39111ce6a12 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -348,11 +348,6 @@ def hosts_count(resource_name = controller.resource_name) @hosts_count ||= HostCounter.new(resource_name) end - def webpack_dev_server - return unless Rails.configuration.webpack.dev_server.enabled - javascript_include_tag "#{@dev_server}/webpack-dev-server.js" - end - def accessible_resource_records(resource, order = :name) klass = resource.to_s.classify.constantize klass = klass.with_taxonomy_scope_override(@location, @organization) if klass.include? Taxonomix diff --git a/app/helpers/layout_helper.rb b/app/helpers/layout_helper.rb index bdef520fc678..9b36fdad0290 100644 --- a/app/helpers/layout_helper.rb +++ b/app/helpers/layout_helper.rb @@ -99,6 +99,29 @@ def javascript(*args) content_for(:javascripts) { javascript_include_tag(*args) } end + def javascript_include_tag(*params, **kwargs) + # Workaround for overriding javascript load with webpack_asset_paths, should be removed when webpack_asset_paths is removed + if kwargs[:source] == "webpack_asset_paths" + kwargs[:webpacked] + else + super(*params, **kwargs) + end + end + + # @deprecated Previously provided by webpack-rails + def webpack_asset_paths(plugin_name, extension: 'js') + if extension == 'js' + Foreman::Deprecation.deprecation_warning('3.12', '`webpack_asset_paths` is deprecated, use `content_for(:javascripts) { webpacked_plugins_js_for(plugin_name) }` instead.') + [{ + source: 'webpack_asset_paths', + webpacked: webpacked_plugins_js_for(plugin_name.to_sym), + }] + elsif extension == 'css' + Foreman::Deprecation.deprecation_warning('3.12', '`webpack_asset_paths` is deprecated and not needed for css assets.') + nil + end + end + # The target should have class="collapse [out|in]" out means collapsed on load and in means expanded. # Target must also have a unique id. def collapsing_header(title, target, collapsed = '') diff --git a/app/helpers/reactjs_helper.rb b/app/helpers/reactjs_helper.rb index 3799bee28a33..f9c04ab8a6d1 100644 --- a/app/helpers/reactjs_helper.rb +++ b/app/helpers/reactjs_helper.rb @@ -1,4 +1,5 @@ -require 'webpack-rails' +require 'json' + module ReactjsHelper # Mount react component in views # Params: @@ -11,10 +12,6 @@ def react_component(name, props = {}) content_tag('foreman-react-component', '', :name => name, :data => { props: props }) end - def webpacked_plugins_with_global_css - global_css_tags(global_plugins_list).join.html_safe - end - def webpacked_plugins_js_for(*plugin_names) js_tags_for(select_requested_plugins(plugin_names)).join.html_safe end @@ -24,7 +21,26 @@ def webpacked_plugins_with_global_js end def webpacked_plugins_css_for(*plugin_names) - css_tags_for(select_requested_plugins(plugin_names)).join.html_safe + Foreman::Deprecation.deprecation_warning('3.12', '`webpacked_plugins_css_for` is deprecated, plugin css is already loaded.') + nil + end + + def read_webpack_manifest + JSON.parse(Rails.root.join('public/webpack/manifest.json').read) + end + + def get_webpack_foreman_vendor_js + Rails.cache.fetch('webpack_foreman_vendor_js', expires_in: 1.minute) do + data = read_webpack_manifest + foreman_vendor_js = data['assetsByChunkName']['foreman-vendor'].find { |value| value.end_with?('.js') } + javascript_include_tag("/webpack/#{foreman_vendor_js}") + end + end + + def get_webpack_foreman_vendor_css + data = read_webpack_manifest + foreman_vendor_css = data['assetsByChunkName']['foreman-vendor'].find { |value| value.end_with?('.css') } + stylesheet_link_tag("/webpack/#{foreman_vendor_css}") end def select_requested_plugins(plugin_names) @@ -39,30 +55,23 @@ def select_requested_plugins(plugin_names) def js_tags_for(requested_plugins) requested_plugins.map do |plugin| - javascript_include_tag(*webpack_asset_paths(plugin.to_s, :extension => 'js')) + name = plugin.to_s.tr('-', '_') + javascript_tag("window.tfm.tools.loadPluginModule('/webpack/#{name}','#{name}','./index');".html_safe) end end def global_js_tags(requested_plugins) requested_plugins.map do |plugin| plugin[:files].map do |file| - javascript_include_tag(*webpack_asset_paths("#{plugin[:id]}:#{file}", :extension => 'js')) - end - end - end - - def global_css_tags(requested_plugins) - requested_plugins.map do |plugin| - plugin[:files].map do |file| - stylesheet_link_tag(*webpack_asset_paths("#{plugin[:id]}:#{file}", :extension => 'css')) + name = plugin[:id].to_s.tr('-', '_') + javascript_tag("window.tfm.tools.loadPluginModule('/webpack/#{name}','#{name}','./#{file}_index');".html_safe) end end end def css_tags_for(requested_plugins) - requested_plugins.map do |plugin| - stylesheet_link_tag(*webpack_asset_paths(plugin.to_s, :extension => 'css')) - end + Foreman::Deprecation.deprecation_warning('3.12', '`css_tags_for` is deprecated, No need to load CSS separately, since it should be referenced from the corresponding JS file.') + [] end def locale_js_tags diff --git a/app/services/foreman/env_settings_loader.rb b/app/services/foreman/env_settings_loader.rb index 19d55419d836..12701c88a5e3 100644 --- a/app/services/foreman/env_settings_loader.rb +++ b/app/services/foreman/env_settings_loader.rb @@ -31,8 +31,6 @@ def settings_map 'FOREMAN_REQUIRE_SSL' => [:boolean, :require_ssl], 'FOREMAN_SUPPORT_JSONP' => [:boolean, :support_jsonp], 'FOREMAN_MARK_TRANSLATED' => [:boolean, :mark_translated], - 'FOREMAN_WEBPACK_DEV_SERVER' => [:boolean, :webpack_dev_server], - 'FOREMAN_WEBPACK_DEV_SERVER_HTTPS' => [:boolean, :webpack_dev_server_https], 'FOREMAN_ASSETS_DEBUG' => [:boolean, :assets_debug], 'FOREMAN_HSTS_ENABLED' => [:boolean, :hsts_enabled], 'FOREMAN_DOMAIN' => [:string, :domain], diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 51f555a29469..0374c02a3a4c 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -10,10 +10,8 @@ <%= favicon_link_tag "favicon.ico"%> - <%= stylesheet_link_tag *webpack_asset_paths('foreman-vendor', :extension => 'css') %> - <%= stylesheet_link_tag *webpack_asset_paths('bundle', :extension => 'css') %> + <%= get_webpack_foreman_vendor_css %> <%= stylesheet_link_tag 'application' %> - <%= webpacked_plugins_with_global_css %> <%= yield(:stylesheets) %> <%= csrf_meta_tags %> @@ -38,13 +36,17 @@ - <%= javascript_include_tag *webpack_asset_paths('foreman-vendor', :extension => 'js') %> - <%= javascript_include_tag *webpack_asset_paths('vendor', :extension => 'js') %> - <%= javascript_include_tag *webpack_asset_paths('bundle', :extension => 'js') %> + <%= get_webpack_foreman_vendor_js %> + <%= javascript_include_tag('/webpack/vendor.js') %> + <%= javascript_include_tag('/webpack/bundle.js') %> + <%= javascript_include_tag 'application' %> <%= webpacked_plugins_with_global_js %> - <%= webpack_dev_server %> - <%= yield(:javascripts) %> + <%= javascript_include_tag('late_load') %> + diff --git a/config/environments/development.rb b/config/environments/development.rb index 9ef5ef9cfb9e..5d5ecc7f8885 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -55,10 +55,6 @@ end end - # Allow disabling the webpack dev server from the settings - config.webpack.dev_server.enabled = SETTINGS.fetch(:webpack_dev_server, true) - config.webpack.dev_server.https = SETTINGS.fetch(:webpack_dev_server_https, false) - config.hosts += SETTINGS[:hosts] config.hosts << SETTINGS[:fqdn] # Backporting from Rails 7.0 diff --git a/config/environments/production.rb b/config/environments/production.rb index 9d318c298559..4788ffe8aa6b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -73,8 +73,6 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - config.webpack.dev_server.enabled = false - # Log denied attributes into logger config.action_controller.action_on_unpermitted_parameters = :log diff --git a/config/environments/test.rb b/config/environments/test.rb index 480205a06a8d..5ed9ce61c2f3 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -64,8 +64,6 @@ # Randomize the order test cases are executed. config.active_support.test_order = :random - config.webpack.dev_server.enabled = false - # Whitelist all plugin engines by default from raising errors on deprecation warnings for # compatibility, allow them to override it by adding an ASDT configuration file. config.after_initialize do diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index bf5ad7054ea1..2e5f32b42293 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,13 +1,3 @@ -module Webpack - module Rails - class Manifest - class << self - attr_writer :manifest - end - end - end -end - # Be sure to restart your server when you modify this file. Foreman::Application.configure do |app| # Version of your assets, change this if you want to expire all your assets. @@ -53,35 +43,5 @@ class << self ActionView::Base.assets_manifest = app.assets_manifest end end - - # When the dev server is enabled, this static manifest file is ignored and - # always retrieved from the dev server. - # - # Otherwise we need to combine all the chunks from the various webpack - # manifests. This is the main foreman manifest and all plugins that may - # have one. We then store this in the webpack-rails manifest using our - # monkey patched function. - unless config.webpack.dev_server.enabled - if (webpack_manifest_file = Dir.glob("#{Rails.root}/public/webpack/manifest.json").first) - webpack_manifest = JSON.parse(File.read(webpack_manifest_file)) - - Foreman::Plugin.with_webpack.each do |plugin| - manifest_path = plugin.webpack_manifest_path - next unless manifest_path - - Rails.logger.debug { "Loading #{plugin.id} webpack asset manifest from #{manifest_path}" } - assets = JSON.parse(File.read(manifest_path)) - - plugin_id = plugin.id.to_s - assets['assetsByChunkName'].each do |chunk, filename| - if chunk == plugin_id || chunk.start_with?("#{plugin_id}:") - webpack_manifest['assetsByChunkName'][chunk] = filename - end - end - end - - Webpack::Rails::Manifest.manifest = webpack_manifest - end - end end end diff --git a/config/settings.yaml.example b/config/settings.yaml.example index 38d71745baab..26059817e3cd 100644 --- a/config/settings.yaml.example +++ b/config/settings.yaml.example @@ -11,15 +11,6 @@ # Mark translated strings with X characters (for developers) :mark_translated: false -# Use the webpack development server? set to false to disable (for developers) -# Make sure to run `rake webpack:compile` if disabled. -:webpack_dev_server: true - -# If you run Foreman in development behind some proxy or use HTTPS you need -# to enable HTTPS for webpack dev server too, otherwise you'd get mixed content -# errors in your browser -:webpack_dev_server_https: false - # Assets in development are not bundled/minified # Do not set this to false if you plan to edit assets (css, js, etc.) :assets_debug: false diff --git a/config/webpack.config.js b/config/webpack.config.js index 66a359ff2d5f..c0d7144e5fb6 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -3,27 +3,31 @@ var path = require('path'); var webpack = require('webpack'); +const dotenv = require('dotenv'); +dotenv.config(); var ForemanVendorPlugin = require('@theforeman/vendor') .WebpackForemanVendorPlugin; -var UglifyJsPlugin = require('uglifyjs-webpack-plugin'); var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; -var ExtractTextPlugin = require('extract-text-webpack-plugin'); -var CompressionPlugin = require('compression-webpack-plugin'); -var pluginUtils = require('../script/plugin_webpack_directories'); var vendorEntry = require('./webpack.vendor'); -var SimpleNamedModulesPlugin = require('../webpack/simple_named_modules'); -var argvParse = require('argv-parse'); var fs = require('fs'); -var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +const { ModuleFederationPlugin } = require('webpack').container; +var pluginUtils = require('../script/plugin_webpack_directories'); -var args = argvParse({ - port: { - type: 'string', - }, - host: { - type: 'string', - }, -}); +class AddRuntimeRequirement { + // to avoid "webpackRequire.l is not a function" error + // enables use of webpack require inside promise new promise + apply(compiler) { + compiler.hooks.compilation.tap('AddRuntimeRequirement', compilation => { + const { RuntimeGlobals } = compiler.webpack; + compilation.hooks.additionalModuleRuntimeRequirements.tap( + 'AddRuntimeRequirement', + (module, set) => { + set.add(RuntimeGlobals.loadScript); + } + ); + }); + } +} const supportedLocales = () => { const localeDir = path.join(__dirname, '..', 'locale'); @@ -42,111 +46,55 @@ const supportedLanguages = () => { return [...new Set(supportedLocales().map(d => d.split('_')[0]))]; }; -const devServerConfig = () => { - const result = require('dotenv').config(); - if (result.error && result.error.code !== 'ENOENT') { - throw result.error; - } +const supportedLanguagesRE = new RegExp( + `/(${supportedLanguages().join('|')})$` +); - return { - port: args.port || '3808', - host: args.host || process.env.BIND || 'localhost', - }; -}; - -module.exports = env => { - const devServer = devServerConfig(); - - // set TARGETNODE_ENV=production on the environment to add asset fingerprints +const commonConfig = function() { var production = process.env.RAILS_ENV === 'production' || process.env.NODE_ENV === 'production'; - - var bundleEntry = path.join( - __dirname, - '..', - 'webpack/assets/javascripts/bundle.js' - ); - - var plugins = pluginUtils.getPluginDirs('pipe'); - - var resolveModules = [ - path.join(__dirname, '..', 'webpack'), - path.join(__dirname, '..', 'node_modules'), - 'node_modules/', - ].concat(pluginUtils.pluginNodeModules(plugins)); - - if (env && env.pluginName !== undefined) { - var pluginEntries = {}; - pluginEntries[env.pluginName] = plugins['entries'][env.pluginName]; - for (var entry of Object.keys(plugins['entries'])) { - if (entry.startsWith(env.pluginName + ':')) { - pluginEntries[entry] = plugins['entries'][entry]; - } - } - - var outputPath = path.join( - plugins['plugins'][env.pluginName]['root'], - 'public', - 'webpack' - ); - var jsFilename = production - ? env.pluginName + '/[name]-[chunkhash].js' - : env.pluginName + '/[name].js'; - var cssFilename = production - ? env.pluginName + '/[name]-[chunkhash].css' - : env.pluginName + '/[name].css'; - var chunkFilename = production - ? env.pluginName + '/[name]-[chunkhash].js' - : env.pluginName + '/[name].js'; - var manifestFilename = env.pluginName + '/manifest.json'; + const mode = production ? 'production' : 'development'; + const config = {}; + if (production) { + config.devtool = 'source-map'; + config.optimization = { + moduleIds: 'named', + splitChunks: false, + }; } else { - var pluginEntries = plugins['entries']; - var outputPath = path.join(__dirname, '..', 'public', 'webpack'); - var jsFilename = production ? '[name]-[chunkhash].js' : '[name].js'; - var cssFilename = production ? '[name]-[chunkhash].css' : '[name].css'; - var chunkFilename = production ? '[name]-[chunkhash].js' : '[name].js'; - var manifestFilename = 'manifest.json'; + config.devtool = 'inline-source-map'; + config.optimization = { + splitChunks: false, + }; } - - var entry = Object.assign( - { - bundle: bundleEntry, - vendor: vendorEntry, - }, - pluginEntries - ); - - const supportedLanguagesRE = new RegExp( - `/(${supportedLanguages().join('|')})$` - ); - - var config = { - entry: entry, - output: { - // Build assets directly in to public/webpack/, let webpack know - // that all webpacked assets start with webpack/ - - // must match config.webpack.output_dir - path: outputPath, - publicPath: '/webpack/', - filename: jsFilename, - chunkFilename, - }, - + return { + ...config, + mode, resolve: { - modules: resolveModules, - alias: Object.assign( - { - foremanReact: path.join( - __dirname, - '../webpack/assets/javascripts/react_app' - ), - }, - pluginUtils.aliasPlugins(pluginEntries) - ), + fallback: { + path: require.resolve('path-browserify'), + os: require.resolve('os-browserify'), + }, + alias: { + foremanReact: path.join( + __dirname, + '../webpack/assets/javascripts/react_app' + ), + '@theforeman/vendor': path.join( + __dirname, + '..', + '..', + 'foreman', + 'node_modules', + '@theforeman', + 'vendor' + ), + }, + }, + resolveLoader: { + modules: [path.resolve(__dirname, '..', 'node_modules')], }, - module: { rules: [ { @@ -159,25 +107,14 @@ module.exports = env => { presets: [require.resolve('@theforeman/builder/babel')], }, }, - { - test: /\.css$/, - use: ExtractTextPlugin.extract({ - fallback: 'style-loader', - use: 'css-loader', - }), - }, { test: /\.(png|gif|svg)$/, - use: 'url-loader?limit=32767', - }, - { - test: /\.scss$/, - use: ExtractTextPlugin.extract({ - fallback: 'style-loader', // The backup style loader - use: production - ? 'css-loader!sass-loader' - : 'css-loader?sourceMap!sass-loader?sourceMap', - }), + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 32767, + }, + }, }, { test: /\.(graphql|gql)$/, @@ -186,48 +123,13 @@ module.exports = env => { }, ], }, - plugins: [ new ForemanVendorPlugin({ - mode: production ? 'production' : 'development', - }), - // must match config.webpack.manifest_filename - new StatsWriterPlugin({ - filename: manifestFilename, - fields: null, - transform: function(data, opts) { - return JSON.stringify( - { - assetsByChunkName: data.assetsByChunkName, - errors: data.errors, - warnings: data.warnings, - }, - null, - 2 - ); - }, - }), - new ExtractTextPlugin({ - filename: cssFilename, - allChunks: true, - }), - new OptimizeCssAssetsPlugin({ - assetNameRegExp: /\.css$/g, - cssProcessor: require('cssnano'), - cssProcessorPluginOptions: { - preset: [ - 'default', - { - discardComments: { removeAll: true }, - discardDuplicates: { removeAll: true }, - }, - ], - }, - canPrint: true, + mode, }), new webpack.DefinePlugin({ 'process.env': { - NODE_ENV: JSON.stringify(production ? 'production' : 'development'), + NODE_ENV: JSON.stringify(mode), NOTIFICATIONS_POLLING: process.env.NOTIFICATIONS_POLLING, REDUX_LOGGER: process.env.REDUX_LOGGER, }, @@ -242,46 +144,176 @@ module.exports = env => { /react-intl\/locale-data/, supportedLanguagesRE ), + new AddRuntimeRequirement(), ], + stats: process.env.WEBPACK_STATS || 'normal', }; +}; - config.plugins.push( - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - minChunks: Infinity, - }) +const coreConfig = function() { + var config = commonConfig(); + var manifestFilename = 'manifest.json'; + var bundleEntry = path.join( + __dirname, + '..', + 'webpack/assets/javascripts/bundle.js' ); + config.context = path.resolve(__dirname, '..'); + config.entry = { + bundle: { import: bundleEntry, dependOn: 'vendor' }, + vendor: vendorEntry, + }; + config.output = { + path: path.join(__dirname, '..', 'public', 'webpack'), + publicPath: '/webpack/', + }; + var plugins = config.plugins; - if (production) { - config.plugins.push( - new webpack.NoEmitOnErrorsPlugin(), - new UglifyJsPlugin({ - uglifyOptions: { - compress: { warnings: false }, + plugins.push( + new ModuleFederationPlugin({ + name: 'foremanReact', + }) + ); + plugins.push( + new StatsWriterPlugin({ + filename: manifestFilename, + }) + ); + config.plugins = plugins; + var rules = config.module.rules; + rules.push({ + test: /\.(sa|sc|c)ss$/, + use: [ + { + loader: 'style-loader', + options: { + injectType: 'singletonStyleTag', + attributes: { id: 'foreman_core' }, }, - sourceMap: true, - }), - new SimpleNamedModulesPlugin(), - new webpack.optimize.ModuleConcatenationPlugin(), - new webpack.optimize.OccurrenceOrderPlugin(), - new CompressionPlugin() + }, + 'css-loader', + 'sass-loader', + ], + }); + config.module.rules = rules; + return config; +}; + +const pluginConfig = function(plugin) { + const pluginRoot = plugin.root; + const pluginName = plugin.name.replace('-', '_'); // module federation doesnt like - + var config = commonConfig(); + config.context = path.join(pluginRoot, 'webpack'); + config.entry = {}; + var pluginEntries = { + './index': path.resolve(pluginRoot, 'webpack', 'index'), + }; + plugin.entries.filter(Boolean).forEach(entry => { + pluginEntries[`./${entry}_index`] = path.resolve( + pluginRoot, + 'webpack', + `${entry}_index` ); - config.devtool = 'source-map'; + }); + + if (config.mode == 'production') { + var outputPath = path.join(pluginRoot, 'public', 'webpack', pluginName); } else { - config.plugins.push( - new webpack.HotModuleReplacementPlugin() // Enable HMR + var outputPath = path.join( + __dirname, + '..', + 'public', + 'webpack', + pluginName ); - - config.devServer = { - host: devServer.host, - port: devServer.port, - headers: { 'Access-Control-Allow-Origin': '*' }, - hot: true, - stats: (process.env.WEBPACK_STATS || 'minimal'), - }; - // Source maps - config.devtool = 'inline-source-map'; } + config.output = { + path: outputPath, + publicPath: '/webpack/' + pluginName + '/', + uniqueName: pluginName, + }; + var configModules = config.resolve.modules || []; + // make webpack to resolve modules from core first + configModules.unshift(path.resolve(__dirname, '..', 'node_modules')); + // add plugin's node_modules to the reslver list + configModules.push(path.resolve(pluginRoot, 'node_modules')); + config.resolve.modules = configModules; + + //get the list of webpack plugins + var plugins = config.plugins; + plugins.push( + new ModuleFederationPlugin({ + name: pluginName, + filename: pluginName + '_remoteEntry.js', + exposes: pluginEntries, + }) + ); + config.plugins = plugins; + var rules = config.module.rules; + rules.push({ + test: /\.(sa|sc|c)ss$/, + use: [ + { + loader: 'style-loader', + options: { + injectType: 'singletonStyleTag', + attributes: { id: pluginName }, + }, + }, + 'css-loader', + 'sass-loader', + ], + }); + config.module.rules = rules; return config; }; + +module.exports = function(env, argv) { + const { pluginName } = env; + var pluginsDirs = pluginUtils.getPluginDirs('pipe'); + var pluginsInfo = {}; + var pluginsConfigEnv = []; + var pluginDirKeys = Object.keys(pluginsDirs.plugins); + if (pluginName) { + pluginDirKeys = pluginDirKeys.filter(key => key.includes(pluginName)); + } + pluginDirKeys.forEach(pluginDirKey => { + const parts = pluginDirKey.split(':'); + const name = parts[0]; + const entry = parts[1]; + if (pluginsInfo[name]) { + pluginsInfo[name].entries.push(entry); + } else { + pluginsInfo[name] = { + name, + entries: [entry], + root: pluginsDirs.plugins[pluginDirKey].root, + }; + } + if (!pluginDirKey.includes(':')) { + const keysWithExtras = pluginDirKeys.filter(key => + key.includes(pluginDirKey + ':') + ); + // for example: {global: true, routes: true} + const pluginExtras = keysWithExtras.map(key => ({ + [key.split(':')[1]]: true, + })); + pluginsConfigEnv.push({ + plugin: { + ...pluginExtras, + name: pluginDirKey, + root: pluginsDirs.plugins[pluginDirKey].root, + }, + }); + } + }); + let configs = []; + const pluginsInfoValues = Object.values(pluginsInfo); + if (pluginsInfoValues.length > 0) { + configs = pluginsInfoValues.map(plugin => pluginConfig(plugin)); + } + if (pluginName) return configs; + + return [coreConfig(env, argv), ...configs]; +}; diff --git a/developer_docs/getting-started.asciidoc b/developer_docs/getting-started.asciidoc index 843502df315b..11f96546f1da 100644 --- a/developer_docs/getting-started.asciidoc +++ b/developer_docs/getting-started.asciidoc @@ -8,55 +8,29 @@ Following steps are required to setup a webpack development environment: -1. **Settings** - There are 2 relevant settings in `config/settings.yml`. At least `webpack_dev_server` should be set to true: -+ -[source,yaml] ----- -# Use the webpack development server? -# Should be set to true if you want to conveniently develop webpack-processed code. -# Make sure to run `rake webpack:compile` if disabled. -:webpack_dev_server: true - -# If you run Foreman in development behind some proxy or use HTTPS you need -# to enable HTTPS for webpack dev server too, otherwise you'd get mixed content -# errors in your browser -:webpack_dev_server_https: true ----- -+ -2. **Dependencies** +1. **Dependencies** Make sure you have all npm dependencies up to date: `npm install` Alternatively you can run the install command with option `--no-optional` which skips packages that aren't required and can save you some space. -3. **Running webpack** +2. **Running webpack** There are several ways of executing webpack: - using [foreman runner](https://github.com/ddollar/foreman): `foreman start` (starts both rails and webpack server) - using `script/foreman-start-dev` (starts rails and webpack server) - executing rails and webpack processes "manually" ```bash - ./node_modules/.bin/webpack-dev-server \ - --config config/webpack.config.js \ - --port 3808 \ - --public $(hostname):3808 + npx webpack \ + --config config/webpack.config.js ``` -4. **Additional config** - Both `foreman start` and `foreman-start-dev` support `WEBPACK_OPTS` environment variable for passing additional options. This is handy for example when you have development setup with Katello and want to use correct certificates. +3. **Additional config** + Both `foreman start` and `foreman-start-dev` support `WEBPACK_OPTS` environment variable for passing additional options. The webpack build is done for Foreman core and plugins at the same time but seperatly, so options like `--anaylze` that start a server for each build will not work. An example of such setup: + [source,bash] ---- - ./node_modules/.bin/webpack-dev-server \ - --config config/webpack.config.js \ - --port 3808 \ - --public $(hostname):3808 \ - --https \ - --key /etc/pki/katello/private/katello-apache.key \ - --cert /etc/pki/katello/certs/katello-apache.crt \ - --cacert /etc/pki/katello/certs/katello-default-ca.crt \ - --watch-poll 1000 # only use for NFS https://community.theforeman.org/t/webpack-watch-over-nfs/10922 + WEBPACK_OPTS='--progress' foreman start webpack ---- + Additionally you can set `NOTIFICATIONS_POLLING` variable to extend the notification polling interval that is 10s by default and can clutter the console. diff --git a/lib/tasks/jenkins.rake b/lib/tasks/jenkins.rake index 34ad6f62c3fa..d120bb1568cc 100644 --- a/lib/tasks/jenkins.rake +++ b/lib/tasks/jenkins.rake @@ -3,7 +3,7 @@ begin namespace :jenkins do task :unit => ['jenkins:setup:minitest', 'rake:test:units', 'rake:test:functionals', 'rake:test:graphql'] - task :integration => ['webpack:compile', 'jenkins:setup:minitest', 'rake:test:integration'] + task :integration => ['webpack:compile', 'assets:precompile', 'jenkins:setup:minitest', 'rake:test:integration'] task :functionals => ["jenkins:setup:minitest", 'rake:test:functionals'] task :external => ['rake:test:external'] task :units => ["jenkins:setup:minitest", 'rake:test:units'] diff --git a/lib/tasks/plugin_assets.rake b/lib/tasks/plugin_assets.rake index 1cfbbeaa7a45..a02a7c842c7e 100644 --- a/lib/tasks/plugin_assets.rake +++ b/lib/tasks/plugin_assets.rake @@ -51,9 +51,8 @@ task 'plugin:assets:precompile', [:plugin] => [:environment] do |t, args| return unless File.exist?("#{@plugin.path}/webpack") return unless File.exist?("#{@plugin.path}/package.json") ENV["NODE_ENV"] ||= 'production' - webpack_bin = ::Rails.root.join('node_modules/webpack/bin/webpack.js') - config_file = ::Rails.root.join(::Rails.configuration.webpack.config_file) - sh "node --max_old_space_size=2048 #{webpack_bin} --config #{config_file} --bail --env.pluginName=#{@plugin.id}" + config_file = Rails.root.join('config', 'webpack.config.js') + sh "npx --max_old_space_size=2048 webpack --config #{config_file} --bail --env pluginName=#{@plugin.id}" end end end diff --git a/lib/tasks/webpack_compile.rake b/lib/tasks/webpack_compile.rake index 9942a4cfff41..4aaa37d538c8 100644 --- a/lib/tasks/webpack_compile.rake +++ b/lib/tasks/webpack_compile.rake @@ -1,6 +1,3 @@ -# We need to delete the existing task which comes from webpack-rails gem or this task will get executed twice -Rake::Task['webpack:compile'].clear - namespace :webpack do # TODO: remove after migrating away from webpack-rails (after setting the # max_old_space_size) in other tool. @@ -11,18 +8,13 @@ namespace :webpack do task compile: :environment do ENV["TARGET"] = 'production' # TODO: Deprecated, use NODE_ENV instead ENV["NODE_ENV"] ||= 'production' - webpack_bin = ::Rails.root.join(::Rails.configuration.webpack.binary) - config_file = ::Rails.root.join(::Rails.configuration.webpack.config_file) + config_file = ::Rails.root.join('config/webpack.config.js') max_old_space_size = "2048" - unless File.exist?(webpack_bin) - raise "Can't find our webpack executable at #{webpack_bin} - have you run `npm install`?" - end - unless File.exist?(config_file) raise "Can't find our webpack config file at #{config_file}" end - sh "node --max_old_space_size=#{max_old_space_size} #{webpack_bin} --config #{config_file} --bail" + sh "npx --max_old_space_size=#{max_old_space_size} webpack --config #{config_file} --bail" end end diff --git a/package-exclude.json b/package-exclude.json index c861a9a44fac..83a568845778 100644 --- a/package-exclude.json +++ b/package-exclude.json @@ -30,8 +30,6 @@ "redux-mock-store", "surge", "webpack-bundle-analyzer", - "webpack-dev-server", - "webpack-dev-server-without-h2", "tabbable", "@adobe/css-tools", "sass" diff --git a/package.json b/package.json index 53ac9a35a148..3c3e2a1da234 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Foreman isn't really a node module, these are just dependencies needed to build the webpack bundle. 'dependencies' are the asset libraries in use and 'devDependencies' are used for the build process.", "private": true, "engines": { - "node": "<16.0.0" + "node": ">14.0.0 <16.0.0" }, "scripts": { "lint": "tfm-lint", @@ -20,10 +20,12 @@ "analyze": "./script/webpack-analyze" }, "dependencies": { + "@module-federation/utilities": "^1.7.0", "@theforeman/vendor": "^12.0.1", "graphql-tag": "^2.11.0", "intl": "~1.2.5", "jed": "^1.1.1", + "os-browserify": "^0.3.0", "react-intl": "^2.8.0" }, "devDependencies": { @@ -38,20 +40,17 @@ "argv-parse": "^1.0.1", "babel-eslint": "^10.0.0", "babel-loader": "^8.0.0", - "compression-webpack-plugin": "~1.1.11", "cross-env": "^5.2.0", - "css-loader": "^0.23.1", - "cssnano": "^4.1.10", + "css-loader": "^6.8.1", + "css-minimizer-webpack-plugin": "^4.2.2", + "cssnano": "^5.0.1", "dotenv": "^5.0.0", "eslint": "^6.7.2", "eslint-plugin-spellcheck": "0.0.17", - "expose-loader": "~0.6.0", - "extract-text-webpack-plugin": "^3.0.0", - "file-loader": "^0.9.0", "graphql": "^15.5.0", "highlight.js": "~9.14.0", - "node-sass": "^4.5.0", - "optimize-css-assets-webpack-plugin": "^3.2.0", + "node-sass": "^8.0.0", + "path-browserify": "^1.0.1", "prettier": "^1.19.1", "pretty-format": "26.6.2", "raw-loader": "^0.5.1", @@ -59,17 +58,15 @@ "react-dnd-test-utils": "^9.4.0", "react-remarkable": "^1.1.3", "redux-mock-store": "^1.2.2", - "sass-loader": "~6.0.6", - "style-loader": "^0.13.1", + "sass": "~1.60.0", + "sass-loader": "^13.3.2", + "style-loader": "^1.3.0", "stylelint": "^9.3.0", "stylelint-config-standard": "^18.0.0", - "uglifyjs-webpack-plugin": "^1.2.2", - "url-loader": "^1.0.1", - "webpack": "^3.4.1", - "webpack-bundle-analyzer": ">=3.3.2", - "webpack-dev-server-without-h2": "^2.11.8", - "webpack-stats-plugin": "^0.1.5", "tabbable": "~5.2.0", - "sass": "~1.60.0" + "webpack": "^5.75.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^5.0.1", + "webpack-stats-plugin": "^1.0.3" } } diff --git a/script/foreman-start-dev b/script/foreman-start-dev index f80ac97638e4..655fcd2c64b9 100755 --- a/script/foreman-start-dev +++ b/script/foreman-start-dev @@ -1,3 +1,3 @@ #!/bin/sh -./node_modules/.bin/webpack-dev-server-without-h2 --config config/webpack.config.js --host "::" $WEBPACK_OPTS & +FILTERED_WEBPACK_OPTS=$(echo $WEBPACK_OPTS | sed -e 's/--key [^ ]*//g' -e 's/--public [^ ]*//g' -e 's/--https [^ ]*//g' -e 's/--cert [^ ]*//g' -e 's/--cacert [^ ]*//g') && npx webpack --config config/webpack.config.js --watch $FILTERED_WEBPACK_OPTS & ./bin/rails server -b \[::\] "$@" diff --git a/test/helpers/reactjs_helper_test.rb b/test/helpers/reactjs_helper_test.rb index 0c0c960afa01..d25fe538cd65 100644 --- a/test/helpers/reactjs_helper_test.rb +++ b/test/helpers/reactjs_helper_test.rb @@ -36,8 +36,8 @@ def webpack_asset_paths(bundle_name, opts) test "should create js for plugins with webpacked js" do res = webpacked_plugins_js_for(:foreman_react, :foreman_angular) - assert res.include?('webpack/foreman_react.js') - assert res.include?('webpack/foreman_angular.js') + assert res.include?('webpack/foreman_react') + assert res.include?('webpack/foreman_angular') end test "should be able to load global js in foreman core" do @@ -46,6 +46,6 @@ def webpack_asset_paths(bundle_name, opts) end res = webpacked_plugins_with_global_js - assert res.include?('webpack/plugin_with_global_js:some_global_file.js') + assert res.include?("'/webpack/plugin_with_global_js','plugin_with_global_js','./some_global_file_index'") end end diff --git a/test/integration/middleware_test.rb b/test/integration/middleware_test.rb deleted file mode 100644 index 49fa92b932f1..000000000000 --- a/test/integration/middleware_test.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'integration_test_helper' - -class MiddlewareIntegrationTest < ActionDispatch::IntegrationTest - test "secure headers are set" do - visit '/' - assert_equal page.response_headers['X-Frame-Options'], 'sameorigin' - assert_equal page.response_headers['X-XSS-Protection'], '1; mode=block' - assert_equal page.response_headers['X-Content-Type-Options'], 'nosniff' - assert_equal page.response_headers['Content-Security-Policy'], \ - "default-src 'self'; child-src 'self'; connect-src 'self' ws: wss:; " + - "img-src 'self' data:; script-src 'unsafe-eval' 'unsafe-inline' " + - "'self'; style-src 'unsafe-inline' 'self'" - end - - context 'webpack dev server is enabled' do - setup do - Rails.configuration.webpack.dev_server.enabled = true - @webpack_url = "#{host}:#{Rails.configuration.webpack.dev_server.port}" - Webpack::Rails::Manifest.stubs(:asset_paths).returns([]) - end - - teardown do - Rails.configuration.webpack.dev_server.enabled = false - end - - test 'it is added the to Content-Security-Policy' do - visit '/' - assert page.response_headers['Content-Security-Policy'].include?(@webpack_url) - end - - test 'it is added Content-Security-Policy on welcome pages' do - visit '/domains/help' - assert page.response_headers['Content-Security-Policy'].include?(@webpack_url) - end - - context 'on unauthorized page requests' do - test 'it is added to the Content-Security-Policy as well' do - logout_admin - visit '/domains' - assert page.response_headers['Content-Security-Policy'].include?(@webpack_url) - end - end - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index 24fbbbc57ca4..76eb32592ae5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -62,6 +62,11 @@ def invalid_name_list "\t", ] end +module ReactjsHelper + def read_webpack_manifest + {"assetsByChunkName" => {"foreman-vendor" => ["foreman-vendor.js", "foreman-vendor.css"]}} + end +end module TestCaseRailsLoggerExtensions def before_setup @@ -147,8 +152,7 @@ class ActionView::TestCase class ActionController::TestCase extend Robottelo::Reporter::TestAttributes include ::BasicRestResponseTest - setup :setup_set_script_name, :set_api_user, :turn_off_login, - :disable_webpack, :set_admin + setup :setup_set_script_name, :set_api_user, :turn_off_login, :set_admin class << self alias_method :test, :it @@ -182,13 +186,6 @@ def set_basic_auth(user, password) @request.env['HTTP_ACCEPT'] = 'application/json' end - # functional tests will fail if assets are not compiled because page - # rendering will try to include the webpack assets path which will throw an - # exception. - def disable_webpack - Webpack::Rails::Manifest.stubs(:asset_paths).returns([]) - end - def with_temporary_settings(**kwargs) old_settings = SETTINGS.slice(*kwargs.keys) begin diff --git a/test/unit/foreman/env_settings_loader_test.rb b/test/unit/foreman/env_settings_loader_test.rb index 3cd981c2a646..a0d391b85f83 100644 --- a/test/unit/foreman/env_settings_loader_test.rb +++ b/test/unit/foreman/env_settings_loader_test.rb @@ -10,8 +10,6 @@ class EnvSettingsLoaderTest < ActiveSupport::TestCase 'FOREMAN_REQUIRE_SSL' => 'true', 'FOREMAN_SUPPORT_JSONP' => 'false', 'FOREMAN_MARK_TRANSLATED' => 'false', - 'FOREMAN_WEBPACK_DEV_SERVER' => 'false', - 'FOREMAN_WEBPACK_DEV_SERVER_HTTPS' => 'false', 'FOREMAN_ASSETS_DEBUG' => 'false', 'FOREMAN_HSTS_ENABLED' => 'false', 'FOREMAN_DOMAIN' => 'example.com', @@ -43,8 +41,6 @@ class EnvSettingsLoaderTest < ActiveSupport::TestCase require_ssl: true, support_jsonp: false, mark_translated: false, - webpack_dev_server: false, - webpack_dev_server_https: false, assets_debug: false, hsts_enabled: false, domain: 'example.com', diff --git a/webpack/assets/javascripts/foreman_tools.js b/webpack/assets/javascripts/foreman_tools.js index 7f4fa5464c8d..40784ab26802 100644 --- a/webpack/assets/javascripts/foreman_tools.js +++ b/webpack/assets/javascripts/foreman_tools.js @@ -6,6 +6,7 @@ /* eslint-disable jquery/no-class */ import $ from 'jquery'; +import { importRemote } from '@module-federation/utilities'; import { sprintf, translate as __ } from './react_app/common/I18n'; import { showLoading, hideLoading } from './foreman_navigation'; @@ -166,3 +167,21 @@ export function highlightTabErrors() { .find('.form-control') .focus(); } + +export const loadPluginModule = async (url, scope, module, plugin = true) => { + if (!window.allPluginsLoaded) { + window.allPluginsLoaded = {}; + } + const name = `${scope}${module}`; + window.allPluginsLoaded[name] = false; + await importRemote({ + url, + scope, + module, + remoteEntryFileName: plugin ? `${scope}_remoteEntry.js` : 'remoteEntry.js', + }); + // tag the plugin as loaded + window.allPluginsLoaded[name] = true; + const loadPlugin = new Event('loadPlugin'); + document.dispatchEvent(loadPlugin); +}; diff --git a/webpack/assets/javascripts/react_app/common/AwaitedMount.js b/webpack/assets/javascripts/react_app/common/AwaitedMount.js new file mode 100644 index 000000000000..bd20993f0d8a --- /dev/null +++ b/webpack/assets/javascripts/react_app/common/AwaitedMount.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import { useState, useEffect } from 'react'; +import store from '../redux'; +import componentRegistry from '../components/componentRegistry'; + +// Mounts a component after all plugins have been imported to make sure that all plugins are available to the component +export const AwaitedMount = ({ component, data, flattenData }) => { + const [mounted, setMounted] = useState(false); + const [mountedComponent, setMountedComponent] = useState(null); + const [allPluginsImported, setAllPluginsImported] = useState( + window.allJsLoaded + ); + async function mountComponent() { + if (componentRegistry.registry[component]) { + setMounted(true); + setMountedComponent( + componentRegistry.markup(component, { + data, + store, + flattenData, + }) + ); + } else if (allPluginsImported) { + const awaitedComponent = componentRegistry.markup(component, { + data, + store, + flattenData, + }); + setMounted(true); + setMountedComponent(awaitedComponent); + } + } + const updateAllPluginsImported = e => { + setAllPluginsImported(true); + }; + useEffect(() => { + document.addEventListener('loadJS', updateAllPluginsImported); + return () => window.removeEventListener('loadJS', updateAllPluginsImported); + }, []); + useEffect(() => { + if (!mounted) mountComponent(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [allPluginsImported]); + useEffect(() => { + // Update the component if the data (props) change + if (allPluginsImported) mountComponent(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + return mounted ? mountedComponent : null; +}; + +AwaitedMount.propTypes = { + component: PropTypes.string.isRequired, + data: PropTypes.object, + flattenData: PropTypes.bool, +}; +AwaitedMount.defaultProps = { + data: {}, + flattenData: false, +}; diff --git a/webpack/assets/javascripts/react_app/common/MountingService.js b/webpack/assets/javascripts/react_app/common/MountingService.js index ee87e5335f1e..ba6f82723227 100644 --- a/webpack/assets/javascripts/react_app/common/MountingService.js +++ b/webpack/assets/javascripts/react_app/common/MountingService.js @@ -1,16 +1,16 @@ import ReactDOM from 'react-dom'; -import store from '../redux'; -import componentRegistry from '../components/componentRegistry'; +import React from 'react'; +import { AwaitedMount } from './AwaitedMount'; export { default as registerReducer } from '../redux/reducers/registerReducer'; function mountNode(component, reactNode, data, flattenData) { ReactDOM.render( - componentRegistry.markup(component, { - data, - store, - flattenData, - }), + , reactNode ); } diff --git a/webpack/assets/javascripts/react_app/components/ForemanModal/ForemanModalContext.js b/webpack/assets/javascripts/react_app/components/ForemanModal/ForemanModalContext.js index b6eb04d2556e..fdf3a866ae8e 100644 --- a/webpack/assets/javascripts/react_app/components/ForemanModal/ForemanModalContext.js +++ b/webpack/assets/javascripts/react_app/components/ForemanModal/ForemanModalContext.js @@ -1,4 +1,5 @@ import { createContext } from 'react'; +import forceSingleton from '../../common/forceSingleton'; // creating context in a separate file to avoid circular imports -export default createContext(null); +export default forceSingleton('ForemanModalContext', () => createContext(null)); diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js index 44ff646d1c33..b366137076b9 100644 --- a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js +++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js @@ -27,7 +27,12 @@ import { useForemanSettings } from '../../../Root/Context/ForemanContext'; import BuildModal from './BuildModal'; import Slot from '../../common/Slot'; -export const ForemanActionsBarContext = createContext(); +import forceSingleton from '../../../common/forceSingleton'; + +export const ForemanActionsBarContext = forceSingleton( + 'ActionsBarContext', + () => createContext() +); const ActionsBar = ({ hostId, diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/CardExpansionContext.js b/webpack/assets/javascripts/react_app/components/HostDetails/CardExpansionContext.js index a6fbc8247d6e..1cb0bc205d8a 100644 --- a/webpack/assets/javascripts/react_app/components/HostDetails/CardExpansionContext.js +++ b/webpack/assets/javascripts/react_app/components/HostDetails/CardExpansionContext.js @@ -1,7 +1,10 @@ import PropTypes from 'prop-types'; import React, { useEffect, useReducer, useCallback } from 'react'; +import forceSingleton from '../../common/forceSingleton'; -export const CardExpansionContext = React.createContext({}); +export const CardExpansionContext = forceSingleton('CardExpansionContext', () => + React.createContext({}) +); const cardExpansionReducer = (state, action) => { // A React reducer, not a Redux one! diff --git a/webpack/simple_named_modules.js b/webpack/simple_named_modules.js deleted file mode 100644 index 8365cd7573f0..000000000000 --- a/webpack/simple_named_modules.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - Simple Named Modules Plugin - Strips relative path up to node_modules/ from the module ID. - This allows for consistent module IDs when building webpack bundles from - differing base paths relative to the node_modules directory. - - Based on NamedModulesPlugin by Tobias Koppers @sokra, originally licensed under - MIT License: http://www.opensource.org/licenses/mit-license.php -*/ -"use strict"; - -class SimpleNamedModulesPlugin { - constructor(options) { - this.options = options || {}; - } - - apply(compiler) { - compiler.plugin("compilation", (compilation) => { - compilation.plugin("before-module-ids", (modules) => { - modules.forEach((module) => { - if(module.id === null && module.libIdent) { - module.id = module.libIdent({ - context: this.options.context || compiler.options.context - }); - if (module.id.includes('node_modules')) { - module.id = module.id.slice(module.id.indexOf('node_modules')) - } - } - }); - }); - }); - } -} - -module.exports = SimpleNamedModulesPlugin;