From ed5fdac81d51e1dd34fd86a4eebbe390e5c2cbfb Mon Sep 17 00:00:00 2001 From: MariaAga Date: Thu, 30 Nov 2023 13:22:09 +0100 Subject: [PATCH] webpack 5 --- Gemfile | 1 - Procfile | 5 +- 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 | 46 +- app/services/foreman/env_settings_loader.rb | 2 - app/views/layouts/base.html.erb | 21 +- 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 | 427 ++++++++++-------- developer_docs/getting-started.asciidoc | 38 +- lib/tasks/jenkins.rake | 2 +- lib/tasks/plugin_assets.rake | 5 +- lib/tasks/webpack_compile.rake | 12 +- package-exclude.json | 2 - package.json | 34 +- 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 +- .../react_app/common/variables.scss | 2 +- .../ImpersonateIcon/ImpersonateIcon.scss | 2 +- .../TaxonomySwitcher/TaxonomyDropdown.scss | 2 +- .../components/LoginPage/LoginPage.scss | 2 +- .../components/PF4/Bookmarks/bookmarks.scss | 2 +- .../PasswordStrength/PasswordStrength.scss | 2 +- .../components/SearchBar/SearchBar.scss | 2 +- .../forms/DateTime/DateTimeOverrides.scss | 2 +- .../components/common/forms/NumericInput.scss | 4 +- webpack/simple_named_modules.js | 35 -- 41 files changed, 495 insertions(+), 490 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 5c22c12a5caf..eefe3873a568 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..8211be6968dd 100644 --- a/Procfile +++ b/Procfile @@ -2,4 +2,7 @@ # 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 +webpack: [ -n "$NODE_ENV" ] && npx webpack --config config/webpack.config.js --watch $WEBPACK_OPTS || env NODE_ENV=development npx webpack --config config/webpack.config.js --watch --analyze +#TODO readme/forklift to change/remove --key to --server-options-key +# --public -> --client-web-socket-url and see why it doesnt work +# --https --> --server-type 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 72b918610d6c..f702dfe1b05c 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 8ca268df7ce9..83e0ca561ea4 100644 --- a/app/helpers/layout_helper.rb +++ b/app/helpers/layout_helper.rb @@ -98,6 +98,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..af2bedbcdccb 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,25 @@ 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.') + 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 +54,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 c1f0f205bd02..d81d28a330b2 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -9,11 +9,8 @@ <%= yield(:meta) %> <%= 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 %> @@ -33,18 +30,20 @@ <%= javascript_include_tag "locale/#{FastGettext.locale}/app" %> <%= locale_js_tags %> - <%= yield(:head) %> - - <%= 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..ee80a046f983 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,175 @@ 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; + + // since CardTemplate uses CardExpansionContext between plugins, we need to make sure there is only one source of it, otherwise multiple contexts will be created and only one will be initialized correctly + const coreShared = { + './webpack/assets/javascripts/react_app/components/HostDetails/Templates/CardItem/CardTemplate/index.js': { + singleton: true, + eager: true, + shareKey: + 'foreman/webpack/assets/javascripts/react_app/components/HostDetails/Templates/CardItem/CardTemplate/index.js', + }, + }; + + plugins.push( + new ModuleFederationPlugin({ + name: 'foremanReact', + shared: { + ...coreShared, + }, + }) + ); + plugins.push( + new StatsWriterPlugin({ + filename: manifestFilename, }) ); + config.plugins = plugins; + var rules = config.module.rules; + rules.push({ + test: /\.(sa|sc|c)ss$/, + use: ['style-loader', 'css-loader', 'sass-loader'], + }); + config.module.rules = rules; + return config; +}; - if (production) { - config.plugins.push( - new webpack.NoEmitOnErrorsPlugin(), - new UglifyJsPlugin({ - uglifyOptions: { - compress: { warnings: false }, - }, - sourceMap: true, - }), - new SimpleNamedModulesPlugin(), - new webpack.optimize.ModuleConcatenationPlugin(), - new webpack.optimize.OccurrenceOrderPlugin(), - new CompressionPlugin() - ); - config.devtool = 'source-map'; - } else { - config.plugins.push( - new webpack.HotModuleReplacementPlugin() // Enable HMR +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.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: path.join(__dirname, '..', 'public', 'webpack', pluginName), + 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; + + const pathToCardTemplate = path.resolve( + __dirname, + '..', + 'webpack/assets/javascripts/react_app/components/HostDetails/Templates/CardItem/CardTemplate/index.js' + ); + const relativePath = path.relative(pluginRoot, pathToCardTemplate); + const pluginShared = { + ['../' + relativePath]: { + singleton: true, + shareKey: + 'foreman/webpack/assets/javascripts/react_app/components/HostDetails/Templates/CardItem/CardTemplate/index.js', + }, + }; + plugins.push( + new ModuleFederationPlugin({ + name: pluginName, + filename: pluginName + '_remoteEntry.js', + shared: { + ...pluginShared, + }, + exposes: pluginEntries, + }) + ); + config.plugins = plugins; + var rules = config.module.rules; + rules.push({ + test: /\.(sa|sc|c)ss$/, + use: ['style-loader', '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..f1e1e68cd773 100644 --- a/developer_docs/getting-started.asciidoc +++ b/developer_docs/getting-started.asciidoc @@ -8,55 +8,31 @@ 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** +3. **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. An example of such setup: + [source,bash] ---- - ./node_modules/.bin/webpack-dev-server \ + npx webpack \ --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 + --watchOptions-poll 1000 # only use for NFS https://community.theforeman.org/t/webpack-watch-over-nfs/10922 ---- + 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..61f4307e29ba 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,16 @@ "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": "10.2.0", + "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" + "url-loader": "4.1.1", + "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..a93987326d71 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 & +npx webpack --config config/webpack.config.js --watch $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/common/variables.scss b/webpack/assets/javascripts/react_app/common/variables.scss index dea052e654ae..4f3e5239507d 100644 --- a/webpack/assets/javascripts/react_app/common/variables.scss +++ b/webpack/assets/javascripts/react_app/common/variables.scss @@ -1,3 +1,3 @@ -@import '~@theforeman/vendor/scss/variables'; +@import 'node_modules/@theforeman/vendor/scss/variables.scss'; $header-max-width: calc(#{$pf-global--breakpoint--lg} + 70px); //TODO move into @theforeman/vendor/scss/variables diff --git a/webpack/assets/javascripts/react_app/components/Layout/components/ImpersonateIcon/ImpersonateIcon.scss b/webpack/assets/javascripts/react_app/components/Layout/components/ImpersonateIcon/ImpersonateIcon.scss index fee44434dbca..31fdfd1efca8 100644 --- a/webpack/assets/javascripts/react_app/components/Layout/components/ImpersonateIcon/ImpersonateIcon.scss +++ b/webpack/assets/javascripts/react_app/components/Layout/components/ImpersonateIcon/ImpersonateIcon.scss @@ -1,4 +1,4 @@ -@import "~@theforeman/vendor/scss/variables"; +@import "node_modules/@theforeman/vendor/scss/variables"; @keyframes blink { 0% { diff --git a/webpack/assets/javascripts/react_app/components/Layout/components/TaxonomySwitcher/TaxonomyDropdown.scss b/webpack/assets/javascripts/react_app/components/Layout/components/TaxonomySwitcher/TaxonomyDropdown.scss index 5f7e814d201c..448bcf664ae2 100644 --- a/webpack/assets/javascripts/react_app/components/Layout/components/TaxonomySwitcher/TaxonomyDropdown.scss +++ b/webpack/assets/javascripts/react_app/components/Layout/components/TaxonomySwitcher/TaxonomyDropdown.scss @@ -1,4 +1,4 @@ -@import '~@theforeman/vendor/scss/variables'; +@import 'node_modules/@theforeman/vendor/scss/variables'; .pf-c-masthead .pf-c-toolbar { .pf-c-context-selector__menu-search { diff --git a/webpack/assets/javascripts/react_app/components/LoginPage/LoginPage.scss b/webpack/assets/javascripts/react_app/components/LoginPage/LoginPage.scss index 0f15a369da50..c1b7a6d7ab8c 100644 --- a/webpack/assets/javascripts/react_app/components/LoginPage/LoginPage.scss +++ b/webpack/assets/javascripts/react_app/components/LoginPage/LoginPage.scss @@ -1,4 +1,4 @@ -@import '~@theforeman/vendor/scss/variables'; +@import 'node_modules/@theforeman/vendor/scss/variables'; $caption_font_weight: 600; $background_image: url('../LoginPage/background.svg'); diff --git a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/bookmarks.scss b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/bookmarks.scss index b41b45aad743..e473a8eb10cc 100644 --- a/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/bookmarks.scss +++ b/webpack/assets/javascripts/react_app/components/PF4/Bookmarks/bookmarks.scss @@ -1,4 +1,4 @@ -@import '~@theforeman/vendor/scss/variables'; +@import 'node_modules/@theforeman/vendor/scss/variables'; .bookmarks-dropdown-item { word-break: break-word; diff --git a/webpack/assets/javascripts/react_app/components/PasswordStrength/PasswordStrength.scss b/webpack/assets/javascripts/react_app/components/PasswordStrength/PasswordStrength.scss index db05e547a3c1..33411cb7a70b 100644 --- a/webpack/assets/javascripts/react_app/components/PasswordStrength/PasswordStrength.scss +++ b/webpack/assets/javascripts/react_app/components/PasswordStrength/PasswordStrength.scss @@ -1,4 +1,4 @@ -@import "~@theforeman/vendor/scss/variables"; +@import "node_modules/@theforeman/vendor/scss/variables"; @import '../../common/colors.scss'; .ReactPasswordStrength { diff --git a/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss b/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss index 1d3c880ace09..c33a3beec46a 100644 --- a/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss +++ b/webpack/assets/javascripts/react_app/components/SearchBar/SearchBar.scss @@ -1,4 +1,4 @@ -@import '~@theforeman/vendor/scss/variables'; +@import 'node_modules/@theforeman/vendor/scss/variables'; .autocomplete-search { width: 100%; diff --git a/webpack/assets/javascripts/react_app/components/common/forms/DateTime/DateTimeOverrides.scss b/webpack/assets/javascripts/react_app/components/common/forms/DateTime/DateTimeOverrides.scss index 31c6f3a6c933..c7c4118fbcff 100644 --- a/webpack/assets/javascripts/react_app/components/common/forms/DateTime/DateTimeOverrides.scss +++ b/webpack/assets/javascripts/react_app/components/common/forms/DateTime/DateTimeOverrides.scss @@ -1,4 +1,4 @@ -@import "~@theforeman/vendor/scss/variables"; +@import "node_modules/@theforeman/vendor/scss/variables"; $screen-md: 992px !default; $screen-md-min: $screen-md !default; diff --git a/webpack/assets/javascripts/react_app/components/common/forms/NumericInput.scss b/webpack/assets/javascripts/react_app/components/common/forms/NumericInput.scss index 1e06824d4cd7..4dac7c188b6a 100644 --- a/webpack/assets/javascripts/react_app/components/common/forms/NumericInput.scss +++ b/webpack/assets/javascripts/react_app/components/common/forms/NumericInput.scss @@ -1,5 +1,5 @@ -@import "~@theforeman/vendor/scss/variables"; -@import "~@theforeman/vendor/scss/mixins"; +@import "node_modules/@theforeman/vendor/scss/variables"; +@import "node_modules/@theforeman/vendor/scss/mixins"; .foreman-numeric-input { position: relative; 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;