From 26bf6bbccc7edb502906ab16b7838a4e077f1899 Mon Sep 17 00:00:00 2001 From: MariaAga Date: Mon, 20 Nov 2023 13:04:59 +0100 Subject: [PATCH] webpack 5 --- Dockerfile | 2 +- Procfile | 5 +- app/assets/config/manifest.js | 1 + app/assets/javascripts/application.js | 11 +- .../javascripts/host_edit_interfaces.js | 6 +- app/assets/javascripts/late_load.js | 51 ++ app/controllers/application_controller.rb | 16 - app/helpers/application_helper.rb | 5 - app/helpers/layout_helper.rb | 24 + app/helpers/reactjs_helper.rb | 44 +- app/views/hosts/console/spice.html.erb | 7 +- app/views/layouts/base.html.erb | 22 +- .../report_templates/report_data.html.erb | 9 +- app/views/users/_form.html.erb | 8 +- bundler.d/assets.rb | 2 +- config/environments/development.rb | 4 - config/environments/production.rb | 4 +- config/environments/test.rb | 2 - config/initializers/assets.rb | 30 - config/webpack.config.js | 558 ++++++++++++------ developer_docs/getting-started.asciidoc | 16 +- lib/tasks/plugin_assets.rake | 31 +- lib/tasks/webpack_compile.rake | 12 +- package-exclude.json | 3 +- package.json | 38 +- script/foreman-start-dev | 2 +- script/get_webpack_shared_files.js | 77 +++ test/helpers/reactjs_helper_test.rb | 6 +- test/integration/middleware_test.rb | 10 +- test/test_helper.rb | 15 +- webpack/assets/javascripts/foreman_tools.js | 19 + .../react_app/common/AwaitedMount.js | 54 ++ .../react_app/common/MountingService.js | 14 +- .../react_app/common/globalIdHelpers.js | 2 + webpack/simple_named_modules.js | 35 -- 35 files changed, 737 insertions(+), 408 deletions(-) create mode 100644 app/assets/javascripts/late_load.js create mode 100644 script/get_webpack_shared_files.js create mode 100644 webpack/assets/javascripts/react_app/common/AwaitedMount.js delete mode 100644 webpack/simple_named_modules.js diff --git a/Dockerfile b/Dockerfile index e5acd3021304..b5e8fe1571dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Base container that is used for both building and running the app FROM quay.io/centos/centos:stream8 as base ARG RUBY_VERSION="2.7" -ARG NODEJS_VERSION="12" +ARG NODEJS_VERSION="14" ENV FOREMAN_FQDN=foreman.example.com ENV FOREMAN_DOMAIN=example.com 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..40e856d4eaa6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -7,9 +7,18 @@ //= require lookup_keys $(function() { - $(document).trigger('ContentLoad'); + $(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){ diff --git a/app/assets/javascripts/host_edit_interfaces.js b/app/assets/javascripts/host_edit_interfaces.js index e8bbb5e38409..5877c387c780 100644 --- a/app/assets/javascripts/host_edit_interfaces.js +++ b/app/assets/javascripts/host_edit_interfaces.js @@ -413,9 +413,9 @@ $(document).on('change', '.interface_mac', function(event) { .find('.interface_mac') .attr('id') ) { - var interface = $('#interfaceModal').find('.interface_mac'); - var mac = interface.val(); - var baseurl = interface.attr('data-url'); + var interface_ = $('#interfaceModal').find('.interface_mac'); + var mac = interface_.val(); + var baseurl = interface_.attr('data-url'); $.ajax({ type: 'GET', url: baseurl + '?mac=' + mac, diff --git a/app/assets/javascripts/late_load.js b/app/assets/javascripts/late_load.js new file mode 100644 index 000000000000..71189283253a --- /dev/null +++ b/app/assets/javascripts/late_load.js @@ -0,0 +1,51 @@ +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) { + if (Object.values(window.allPluginsLoaded).every(Boolean)) { + resolve(); + } else { + function handleLoad() { + if (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 cb6f62fb6099..67da735bbb49 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -331,11 +331,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..7ed1a1dd81f3 100644 --- a/app/helpers/layout_helper.rb +++ b/app/helpers/layout_helper.rb @@ -98,6 +98,30 @@ 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 + + + def webpack_asset_paths(plugin_name, extension: 'js') + # @deprecated Previously provided by webpack-rails + 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..42399beadd7c 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.9', '`webpacked_plugins_css_for` is deprecated, plugin css is already loaded.') + end + + def read_webpack_manifest + root = File.expand_path(File.dirname(__FILE__) + "/../..") + file = File.read(root + '/public/webpack/manifest.json') + JSON.parse(file) + end + + def get_webpack_foreman_vendor_js + 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 + + 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,21 @@ 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')) + javascript_tag("window.tfm.tools.loadPluginModule('/webpack/#{plugin.to_s.tr('-', '_')}','#{plugin.to_s.tr('-', '_')}','./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')) + javascript_tag("window.tfm.tools.loadPluginModule('/webpack/#{plugin[:id].to_s.tr('-', '_')}','#{plugin[:id].to_s.tr('-', '_')}','./#{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 referneced from the corresponding JS file.') + [] end def locale_js_tags diff --git a/app/views/hosts/console/spice.html.erb b/app/views/hosts/console/spice.html.erb index 91de1bb86484..cb16f174c33c 100644 --- a/app/views/hosts/console/spice.html.erb +++ b/app/views/hosts/console/spice.html.erb @@ -14,5 +14,8 @@
<% end %> - - +<% content_for(:javascripts) do %> + +<% end %> diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index c1f0f205bd02..01cc2ac5e442 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,21 @@ <%= javascript_include_tag "locale/#{FastGettext.locale}/app" %> <%= locale_js_tags %> - + <%= stylesheet_link_tag('/webpack/bundle', :extension => 'css') %> <%= 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/app/views/report_templates/report_data.html.erb b/app/views/report_templates/report_data.html.erb index 25ab18445104..c87dd443f601 100644 --- a/app/views/report_templates/report_data.html.erb +++ b/app/views/report_templates/report_data.html.erb @@ -1,7 +1,8 @@ <% title _("Download generated report") %> <%= react_component('TemplateGenerator', data: { templateName: @template.name }) %> - - +<% content_for(:javascripts) do %> + +<% end %> diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index 16a6895c9f35..6c6c895a7ad0 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -140,9 +140,11 @@ <% end %> <% if @user.cached_usergroups.any? %> - + }); + + <% end %> <% end %> diff --git a/bundler.d/assets.rb b/bundler.d/assets.rb index 65bff536b50f..dd23e51b5f24 100644 --- a/bundler.d/assets.rb +++ b/bundler.d/assets.rb @@ -4,7 +4,7 @@ gem 'gettext_i18n_rails_js', '~> 1.4' gem 'po_to_json', '~> 1.1' gem 'execjs', '>= 1.4.0', '< 3.0' - gem 'uglifier', '>= 1.0.3' + gem "terser", "~> 1.1" gem 'sass-rails', '~> 6.0' # this one is a dependecy for x-editable-rails gem 'coffee-rails', '~> 5.0.0' 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 f410ff8086f4..4788ffe8aa6b 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -19,7 +19,7 @@ config.public_file_server.enabled = ENV.fetch('RAILS_SERVE_STATIC_FILES', false) == 'true' # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = :terser # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. @@ -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..2901d243a364 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -53,35 +53,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/webpack.config.js b/config/webpack.config.js index 66a359ff2d5f..63e5de8da056 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -3,27 +3,33 @@ 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 MiniCssExtractPlugin = require('mini-css-extract-plugin'); 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'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') + .BundleAnalyzerPlugin; +const { getForemanFilePaths } = require('../script/get_webpack_shared_files'); -var args = argvParse({ - port: { - type: 'string', - }, - host: { - type: 'string', - }, -}); +class AddRuntimeRequirement { + 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 +48,61 @@ 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; - } - - return { - port: args.port || '3808', - host: args.host || process.env.BIND || 'localhost', - }; -}; +const supportedLanguagesRE = new RegExp( + `/(${supportedLanguages().join('|')})$` +); -module.exports = env => { - const devServer = devServerConfig(); - - // set TARGETNODE_ENV=production on the environment to add asset fingerprints +var bundleEntry = path.join( + __dirname, + '..', + 'webpack/assets/javascripts/bundle.js' +); +const commonConfig = function(env, argv) { 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.optimization = { + splitChunks: false, + }; + config.devtool = 'inline-source-map'; } - - 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 +115,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 +131,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 +152,304 @@ module.exports = env => { /react-intl\/locale-data/, supportedLanguagesRE ), + new AddRuntimeRequirement(), + // new webpack.optimize.ModuleConcatenationPlugin(), ], + infrastructureLogging: { + colors: true, + level: 'verbose', + }, + stats: { + logging: 'verbose', + preset: 'verbose', + }, }; +}; + +const moduleFederationSharedConfig = function(env, argv) { + return { + react: { singleton: true }, + 'react-dom': { singleton: true }, + '@theforeman/vendor': { singleton: true }, + '@theforeman/vendor-dev': { singleton: true }, + '@theforeman/vendor-scss': { singleton: true }, + '@theforeman/vendor-scss-variables': { singleton: true }, + '@babel/core': { singleton: true }, + webpack: { singleton: true }, + 'bundle.js': { singleton: true }, + 'path-browserify': { singleton: true }, + 'os-browserify': { singleton: true }, + }; +}; - config.plugins.push( - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor', - minChunks: Infinity, +const coreConfig = function(env, argv) { + var config = commonConfig(env, argv); + + var manifestFilename = 'manifest.json'; + 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; + + const webpackDirectory = path.resolve( + __dirname, + '..', + 'webpack', + 'assets', + 'javascripts', + 'react_app' + ); + const coreShared = {}; + + getForemanFilePaths(false).forEach(file => { + const key = file.replace(webpackDirectory, '').replace(/\.(js|jsx)$/, ''); + coreShared['./' + file] = { + singleton: true, + eager: true, + shareKey: key, + }; + }); + plugins.push( + new ModuleFederationPlugin({ + name: 'foremanReact', + shared: { + ...moduleFederationSharedConfig(env, argv), + + ...coreShared, + }, + }) + ); + plugins.push( + new MiniCssExtractPlugin({ + ignoreOrder: true, + filename: '[name].css', + chunkFilename: '[id].css', + }) + ); + plugins.push( + new StatsWriterPlugin({ + filename: manifestFilename, + fields: null, + transform: function(data, opts) { + return JSON.stringify( + { + assetsByChunkName: data.assetsByChunkName, + errors: data.errors, + warnings: data.warnings, + }, + null, + 2 + ); + }, }) ); - if (production) { - config.plugins.push( - new webpack.NoEmitOnErrorsPlugin(), - new UglifyJsPlugin({ - uglifyOptions: { - compress: { warnings: false }, + plugins.push( + new BundleAnalyzerPlugin({ + generateStatsFile: true, + analyzerMode: 'static', + openAnalyzer: false, + statsFilename: 'stats.json', + }) + ); + config.plugins = plugins; + var rules = config.module.rules; + rules.push({ + test: /\.(sa|sc|c)ss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + options: { + publicPath: path.join(__dirname, '..', 'public', 'webpack'), }, - 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 + }, + 'css-loader', + 'sass-loader', + ], + }); + config.module.rules = rules; + return config; +}; + +const pluginConfig = function(env, argv) { + var pluginEnv = env.plugin; + const pluginRoot = pluginEnv.root; + const pluginName = pluginEnv.name.replace('-', '_'); // module federation doesnt like - + var config = commonConfig(env, argv); + config.context = path.join(pluginRoot, 'webpack'); + config.entry = {}; + var pluginEntries = { + './index': path.resolve(pluginRoot, 'webpack', 'index'), + }; + pluginEnv.entries.filter(Boolean).forEach(entry => { + pluginEntries[`./${entry}_index`] = path.resolve( + pluginRoot, + 'webpack', + `${entry}_index` ); + }); + + config.output = { + path: path.join(__dirname, '..', 'public', 'webpack', pluginName), + publicPath: '/webpack/' + pluginName + '/', + uniqueName: pluginName, + }; - config.devServer = { - host: devServer.host, - port: devServer.port, - headers: { 'Access-Control-Allow-Origin': '*' }, - hot: true, - stats: (process.env.WEBPACK_STATS || 'minimal'), + 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 webpackDirectory = path.resolve( + __dirname, + '..', + 'webpack', + 'assets', + 'javascripts', + 'react_app' + ); + + const pluginShared = {}; + getForemanFilePaths(true).forEach(file => { + const key = file + .replace(webpackDirectory, '') + .replace(/\.(js|jsx)$/, '') + .replace('../../foreman/', ''); + pluginShared[file] = { + singleton: true, + shareKey: key, }; - // Source maps - config.devtool = 'inline-source-map'; - } + }); + const keys = [...Object.keys(pluginShared)]; + const newObj = {}; + keys.forEach(key => { + newObj[key] = pluginShared[key]; + }); + plugins.push( + new ModuleFederationPlugin({ + name: pluginName, + filename: pluginName + '_remoteEntry.js', + shared: { + ...moduleFederationSharedConfig(env, argv), + ...pluginShared, + }, + exposes: pluginEntries, + }) + ); + plugins.push( + new MiniCssExtractPlugin({ + ignoreOrder: true, + filename: pluginName + '/[name].css', + chunkFilename: pluginName + '/[id].css', + }) + ); + const manifestFilename = pluginName + '_manifest.json'; + plugins.push( + new StatsWriterPlugin({ + filename: manifestFilename, + fields: null, + transform: function(data, opts) { + return JSON.stringify( + { + assetsByChunkName: data.assetsByChunkName, + errors: data.errors, + warnings: data.warnings, + }, + null, + 2 + ); + }, + }) + ); + plugins.push( + new BundleAnalyzerPlugin({ + generateStatsFile: true, + analyzerMode: 'static', + openAnalyzer: false, + statsFilename: pluginName + '_stats.json', + }) + ); + config.plugins = plugins; + var rules = config.module.rules; + rules.push({ + test: /\.(sa|sc|c)ss$/, + use: [ + { + loader: MiniCssExtractPlugin.loader, + }, + 'css-loader', + 'sass-loader', + ], + }); + config.module.rules = rules; + config.optimization = { + ...config.optimization, + }; return config; }; + +module.exports = function(env, argv) { + var pluginsDirs = pluginUtils.getPluginDirs('pipe'); + var pluginsInfo = {}; + var pluginsConfigEnv = []; + var pluginDirKeys = Object.keys(pluginsDirs.plugins); + 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: { + // routes: pluginDirKeys.includes(pluginDirKey + ':routes'), + // global: pluginDirKeys.includes(pluginDirKey + ':global'), + ...pluginExtras, + name: pluginDirKey, + root: pluginsDirs.plugins[pluginDirKey].root, + }, + }); + } + }); + + console.log('CHECK CSS') + if (fs.existsSync('node_modules/@theforeman/vendor/scss/variables.scss')) { + console.log('The file node_modules/@theforeman/vendor/scss/variables.scss exists.'); + } else { + console.log('The file node_modules/@theforeman/vendor/scss/variables.scss does not exist.'); + } + let configs = []; + const pluginsInfoValues = Object.values(pluginsInfo); + if (pluginsInfoValues.length > 0) { + configs = pluginsInfoValues.map(plugin => + pluginConfig({ ...env, plugin }, argv) + ); + } + return [coreConfig(env, argv), ...configs]; +}; diff --git a/developer_docs/getting-started.asciidoc b/developer_docs/getting-started.asciidoc index 843502df315b..975767535ff6 100644 --- a/developer_docs/getting-started.asciidoc +++ b/developer_docs/getting-started.asciidoc @@ -36,10 +36,8 @@ Following steps are required to setup a webpack development environment: - 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** @@ -48,15 +46,9 @@ Following steps are required to setup a webpack development environment: + [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/plugin_assets.rake b/lib/tasks/plugin_assets.rake index 1cfbbeaa7a45..18ca415e76be 100644 --- a/lib/tasks/plugin_assets.rake +++ b/lib/tasks/plugin_assets.rake @@ -1,3 +1,5 @@ +require 'shellwords' + desc 'Compile plugin assets - called via rake plugin:assets:precompile[plugin_name]' task 'plugin:assets:precompile', [:plugin] => [:environment] do |t, args| # This task will generate assets for a plugin and namespace them in @@ -43,17 +45,28 @@ task 'plugin:assets:precompile', [:plugin] => [:environment] do |t, args| class PluginWebpackTask attr_accessor :plugin - def initialize(plugin_id) - @plugin = Foreman::Plugin.find(plugin_id) or raise("Unable to find registered plugin #{plugin_id}") + def initialize(plugin_id, plugin: nil) + @plugin = plugin || Foreman::Plugin.find(plugin_id) or raise("Unable to find registered plugin #{plugin_id}") end def compile 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}" + # TODO: delete this as all plugins get compiled in the regura + sh "npx --max_old_space_size=#{max_old_space_size} webpack --config #{config_file} --bail #{webpack_plugin_env}" + end + + def plugin_config + { + name: @plugin.id, + root: @plugin.engine.paths.path.to_s, + entries: @plugin.global_js_files.join(','), + }.compact_blank + end + + def webpack_plugin_env + plugin_config.map { |prop, value| "--env plugin.#{prop}=#{Shellwords.escape(value)}" }.join(' ') end end end @@ -65,6 +78,12 @@ task 'plugin:assets:precompile', [:plugin] => [:environment] do |t, args| task = Foreman::PluginWebpackTask.new(args[:plugin]) task.compile else - puts "You must specify the name of the plugin (e.g. rake plugin:assets:precompile['my_plugin'])" + # puts "You must specify the name of the plugin (e.g. rake plugin:assets:precompile['my_plugin'])" + puts "Plugin not specified, compiling all webpack plugins" + + Foreman::Plugin.with_webpack.each do |plugin| + task = Foreman::PluginWebpackTask.new(nil, plugin: plugin) + task.compile + 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..e9530f094658 100644 --- a/package-exclude.json +++ b/package-exclude.json @@ -30,8 +30,7 @@ "redux-mock-store", "surge", "webpack-bundle-analyzer", - "webpack-dev-server", - "webpack-dev-server-without-h2", + "webpack-stats-plugin", "tabbable", "@adobe/css-tools", "sass" diff --git a/package.json b/package.json index c99562e59c6b..d507764227fd 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": ">12.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,21 @@ "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", + "css-loader": "4.3.0", + "css-minimizer-webpack-plugin": "^4.2.2", "cssnano": "^4.1.10", "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", + "mini-css-extract-plugin": "2.7.5", + "node-sass": "6.0.0", + "optimize-css-assets-webpack-plugin": "^4.0.0", + "path-browserify": "^1.0.1", + "postcss-loader": "4.3.0", + "postcss": "^8.4.12", "prettier": "^1.19.1", "pretty-format": "26.6.2", "raw-loader": "^0.5.1", @@ -59,17 +62,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", - "stylelint": "^9.3.0", + "sass-loader": "10.2.0", + "sass": "~1.60.0", + "style-loader": "1.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", + "stylelint": "^9.3.0", "tabbable": "~5.2.0", - "sass": "~1.60.0" + "url-loader": "4.1.1", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "5.0.1", + "webpack-stats-plugin": "^1.0.3", + "webpack": "5.75.0" } } 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/script/get_webpack_shared_files.js b/script/get_webpack_shared_files.js new file mode 100644 index 000000000000..52716b26a645 --- /dev/null +++ b/script/get_webpack_shared_files.js @@ -0,0 +1,77 @@ +const path = require('path'); +const fs = require('fs'); + +function getForemanFilePaths(isPlugin) { + // 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 + // This is a function in case we want to add more directories in the future + const paths = [ + path.join( + process.cwd(), + './webpack/assets/javascripts/react_app/components/HostDetails/Templates' + ), + ]; + const acc = []; + paths.forEach(_path => { + acc.push(...getForemanFilePath(isPlugin, _path)); + }); + return acc; +} +function getForemanFilePath(isPlugin, directory) { + const files = fs.readdirSync(directory); + const filePaths = files.reduce((acc, file) => { + const filePath = path.join(directory, file); + const isFile = fs.statSync(filePath).isFile(); + if (isFile) { + if (file.endsWith('.js') && !file.includes('test')) { + if (isPlugin) { + acc.push( + path.join('../../foreman', filePath.replace(process.cwd(), '')) + ); + } else { + acc.push(path.join('./', filePath.replace(process.cwd(), ''))); + } + } + } else { + const subDirectoryFiles = getForemanFilePathsRecursive( + filePath, + isPlugin + ); + acc.push(...subDirectoryFiles); + } + return acc; + }, []); + return filePaths; +} + +function getForemanFilePathsRecursive(directory, isPlugin) { + const files = fs.readdirSync(directory); + + const filePaths = files.reduce((acc, file) => { + const filePath = path.join(directory, file); + const isFile = fs.statSync(filePath).isFile(); + if (isFile) { + if (file.endsWith('.js') && !file.includes('test')) { + if (isPlugin) { + acc.push( + path.join('../../foreman', filePath.replace(process.cwd(), '')) + ); + } else { + acc.push(path.join('./', filePath.replace(process.cwd(), ''))); + } + } + } else { + const subDirectoryFiles = getForemanFilePathsRecursive( + filePath, + isPlugin + ); + acc.push(...subDirectoryFiles); + } + return acc; + }, []); + + return filePaths; +} + +module.exports = { + getForemanFilePaths, +}; 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 index 49fa92b932f1..ba0b3309bb6b 100644 --- a/test/integration/middleware_test.rb +++ b/test/integration/middleware_test.rb @@ -14,13 +14,15 @@ class MiddlewareIntegrationTest < ActionDispatch::IntegrationTest 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([]) + @webpack_url = 'test' + # TODO: fix/remove + # 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 + # Rails.configuration.webpack.dev_server.enabled = false end test 'it is added the to Content-Security-Policy' do 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/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..23df91faf43a --- /dev/null +++ b/webpack/assets/javascripts/react_app/common/AwaitedMount.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React, { useState, useEffect } from 'react'; +import store from '../redux'; +import componentRegistry from '../components/componentRegistry'; +import { translate as __ } from './I18n'; + +// 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(false); + 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]); + return mounted ? mountedComponent :
{__('Loading...')}
; +}; + +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/globalIdHelpers.js b/webpack/assets/javascripts/react_app/common/globalIdHelpers.js index 53854715bf42..460ab4b9c754 100644 --- a/webpack/assets/javascripts/react_app/common/globalIdHelpers.js +++ b/webpack/assets/javascripts/react_app/common/globalIdHelpers.js @@ -8,6 +8,8 @@ */ import { Buffer } from 'buffer'; +import { Buffer } from 'buffer'; + const idSeparator = '-'; const versionSeparator = ':'; const defaultVersion = '01'; 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;