From 778f49154d8d638b2f478b5a4bf5e690cf99b3c0 Mon Sep 17 00:00:00 2001 From: MariaAga Date: Fri, 17 Nov 2023 17:20:13 +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 | 22 + 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 | 2 +- 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 + .../react_app/common/variables.scss | 2 +- .../common/DateTimePicker/DateTimePicker.js | 2 +- webpack/simple_named_modules.js | 35 -- 37 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 e5acd302130..b5e8fe1571d 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 f1791e16f5e..8211be6968d 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 e06b86a6f2f..91678641616 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 f08473375e4..40e856d4eaa 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 e8bbb5e3840..5877c387c78 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 00000000000..71189283253 --- /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 d1bc1dcdadd..9321bf5811d 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 cb6f62fb609..67da735bbb4 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 8ca268df7ce..8c7f170ae08 100644 --- a/app/helpers/layout_helper.rb +++ b/app/helpers/layout_helper.rb @@ -98,6 +98,28 @@ def javascript(*args) content_for(:javascripts) { javascript_include_tag(*args) } end + def javascript_include_tag(*params, **kwargs) + # Workaround for overriding javasctipt load with webpack_asset_paths, should be removed when webpack_asset_paths is removed + if kwargs.is_a?(Hash) && kwargs[:source] == "webpack_asset_paths" + kwargs[:webpacked] + else + super(*params, **kwargs) + end + end + + 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 3799bee28a3..42399beadd7 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 91de1bb8648..cb16f174c33 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 c1f0f205bd0..01cc2ac5e44 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 25ab1844510..c87dd443f60 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 16a6895c9f3..6c6c895a7ad 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 65bff536b50..dd23e51b5f2 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 9ef5ef9cfb9..5d5ecc7f888 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 f410ff8086f..9d318c29855 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. diff --git a/config/environments/test.rb b/config/environments/test.rb index 480205a06a8..5ed9ce61c2f 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 bf5ad7054ea..2901d243a36 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 66a359ff2d5..63e5de8da05 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 843502df315..975767535ff 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 1cfbbeaa7a4..18ca415e76b 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 9942a4cfff4..4aaa37d538c 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 c861a9a44fa..e9530f09465 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 c99562e59c6..d507764227f 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 f80ac97638e..a93987326d7 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 00000000000..52716b26a64 --- /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 0c0c960afa0..d25fe538cd6 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 49fa92b932f..ba0b3309bb6 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 24fbbbc57ca..76eb32592ae 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 7f4fa5464c8..40784ab2680 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 00000000000..23df91faf43 --- /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 ee87e5335f1..ba6f8272322 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 20204ca16ac..59be48e9b62 100644 --- a/webpack/assets/javascripts/react_app/common/globalIdHelpers.js +++ b/webpack/assets/javascripts/react_app/common/globalIdHelpers.js @@ -7,6 +7,8 @@ * together into a single string and encoded as base64. */ +import { Buffer } from 'buffer'; + const idSeparator = '-'; const versionSeparator = ':'; const defaultVersion = '01'; diff --git a/webpack/assets/javascripts/react_app/common/variables.scss b/webpack/assets/javascripts/react_app/common/variables.scss index dea052e654a..4f3e5239507 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/common/DateTimePicker/DateTimePicker.js b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js index b947718327b..25e2c3ac396 100644 --- a/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js +++ b/webpack/assets/javascripts/react_app/components/common/DateTimePicker/DateTimePicker.js @@ -93,7 +93,7 @@ class DateTimePicker extends React.Component { this.setState({ hiddenValue: false })} > diff --git a/webpack/simple_named_modules.js b/webpack/simple_named_modules.js deleted file mode 100644 index 8365cd7573f..00000000000 --- 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;