diff --git a/.npmrc b/.npmrc index d5831dd5188..b6f27f13595 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1 @@ engine-strict=true -legacy-peer-deps=true diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 25babb1b055..582ed1bb9c0 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -80,7 +80,7 @@ function onContentLoad() { password_caps_lock_hint(); tfm.i18n.intl.ready.then(function() { - var tz = jstz.determine(); + var tz = tfm.jstz.determine(); $.cookie('timezone', tz.name(), { path: '/', secure: location.protocol === 'https:', diff --git a/app/assets/javascripts/host_edit.js b/app/assets/javascripts/host_edit.js index 7c2f18ab8b6..19eeb147e13 100644 --- a/app/assets/javascripts/host_edit.js +++ b/app/assets/javascripts/host_edit.js @@ -402,12 +402,12 @@ function serializeForm() { } function subnet_contains(network, cidr, ip) { - if (!ip || 0 === ip.length || !ipaddr.isValid(ip)) { + if (!ip || 0 === ip.length || !tfm.ipaddr.isValid(ip)) { return; } - var addr = ipaddr.parse(ip); - var range = ipaddr.parse(network); + var addr = tfm.ipaddr.parse(ip); + var range = tfm.ipaddr.parse(network); return addr.match(range, cidr); } diff --git a/app/assets/javascripts/subnets.js b/app/assets/javascripts/subnets.js index 19db0e569cd..943e67bd430 100644 --- a/app/assets/javascripts/subnets.js +++ b/app/assets/javascripts/subnets.js @@ -69,7 +69,7 @@ function subnetMaskChanged(field) { } if ($('input[id^=subnet_type_]:checked').val() === 'Subnet::Ipv4') { try { - var cidr = ipaddr.IPv4.parse(mask).prefixLengthFromSubnetMask(); + var cidr = tfm.ipaddr.IPv4.parse(mask).prefixLengthFromSubnetMask(); } catch (err) { var cidr = ''; } diff --git a/app/helpers/reactjs_helper.rb b/app/helpers/reactjs_helper.rb index a09381c2372..6afdc4f5ec4 100644 --- a/app/helpers/reactjs_helper.rb +++ b/app/helpers/reactjs_helper.rb @@ -36,13 +36,8 @@ def get_webpack_chunk(name, extension) data['assetsByChunkName'][name]&.find { |value| value.end_with?(".#{extension}") } end - def get_webpack_foreman_vendor_js - foreman_vendor_js = get_webpack_chunk('foreman-vendor', 'js') - javascript_include_tag("/webpack/#{foreman_vendor_js}") - end - def get_webpack_foreman_vendor_css - foreman_vendor_css = get_webpack_chunk('foreman-vendor', 'css') + foreman_vendor_css = get_webpack_chunk('vendorStyles', 'css') stylesheet_link_tag("/webpack/#{foreman_vendor_css}") end diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 1fe19a38a5c..e94f1e5df54 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -10,6 +10,7 @@ <%= favicon_link_tag "favicon.ico"%> + <%= get_webpack_foreman_vendor_css %> <%= stylesheet_link_tag 'application' %> <%= yield(:stylesheets) %> @@ -35,10 +36,10 @@ - <%= get_webpack_foreman_vendor_js %> + <%= javascript_include_tag("/webpack/#{get_webpack_chunk('vendor', 'js')}") %> - <%= javascript_include_tag("/webpack/#{get_webpack_chunk('bundle', 'js')}") %> <%= javascript_include_tag("/webpack/#{get_webpack_chunk('reactExports', 'js')}") %> + <%= javascript_include_tag("/webpack/#{get_webpack_chunk('bundle', 'js')}") %> <%= javascript_include_tag 'application' %> <%= webpacked_plugins_with_global_js %> diff --git a/config/webpack.config.js b/config/webpack.config.js index 17c8186432e..cac3f52b391 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -4,9 +4,10 @@ var path = require('path'); var webpack = require('webpack'); const dotenv = require('dotenv'); +const root = path.resolve(__dirname, '..'); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); + dotenv.config(); -var ForemanVendorPlugin = require('@theforeman/vendor') - .WebpackForemanVendorPlugin; var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; var vendorEntry = require('./webpack.vendor'); var fs = require('fs'); @@ -15,6 +16,18 @@ var pluginUtils = require('../script/plugin_webpack_directories'); var { generateExportsFile }= require('../webpack/assets/javascripts/exportAll'); var CompressionPlugin = require('compression-webpack-plugin'); +const packageJsonPath = path.resolve(__dirname,'..', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +const dependencies = packageJson.dependencies || {}; +const devDependencies = packageJson.devDependencies || {}; +const allDependencies = { ...dependencies, ...devDependencies }; +const shared = isPlugin => Object.keys(allDependencies).map(dep => ({ + [dep]: { + eager: !isPlugin, // core should load all dependencies eagerly so they will be available for plugins + singleton: true, + requiredVersion: allDependencies[dep], + }})); + class AddRuntimeRequirement { // to avoid "webpackRequire.l is not a function" error // enables use of webpack require inside promise new promise @@ -79,11 +92,18 @@ const commonConfig = function() { os: require.resolve('os-browserify'), }, alias: { + 'patternfly-react$': path.resolve(__dirname,'..', 'node_modules/patternfly-react/dist/js/index.js'), // to avoid circular dependency in dist/esm + '/node_modules/jquery': path.resolve(__dirname, '..', 'webpack/assets/javascripts/jquery.js'), + 'jquery': path.resolve(__dirname, '..', 'webpack/assets/javascripts/jquery.js'), foremanReact: path.join( __dirname, '../webpack/assets/javascripts/react_app' ), + 'react/jsx-runtime': 'react/jsx-runtime.js', // for react-dnd + 'react/jsx-dev-runtime': 'react/jsx-dev-runtime.js', // for react-dnd + }, + }, resolveLoader: { modules: [path.resolve(__dirname, '..', 'node_modules')], @@ -117,9 +137,6 @@ const commonConfig = function() { ], }, plugins: [ - new ForemanVendorPlugin({ - mode, - }), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(mode), @@ -160,13 +177,18 @@ const coreConfig = function() { } config.entry = { + /* keep bundle entry files and reactExports seperate to avoid late loading issues of mixed files, import in react_app only from react_app and node_modules */ bundle: { import: bundleEntry, dependOn: ['vendor', 'reactExports'] }, - vendor: vendorEntry, - reactExports: path.join( + reactExports: { + import: path.join( __dirname, '..', 'webpack/assets/javascripts/all_react_app_exports.js' - ), + ), + dependOn: 'vendor', + }, + vendor: vendorEntry, + vendorStyles: path.join(__dirname, '..', 'webpack/assets/javascripts/react_app/common/scss/vendor-core.scss'), }; config.output = { path: path.join(__dirname, '..', 'public', 'webpack'), @@ -179,9 +201,12 @@ const coreConfig = function() { }; var plugins = config.plugins; + plugins.push( + new MiniCssExtractPlugin()); plugins.push( new ModuleFederationPlugin({ name: 'foremanReact', + shared: shared(false), }) ); plugins.push( @@ -191,8 +216,10 @@ const coreConfig = function() { ); config.plugins = plugins; var rules = config.module.rules; + rules.push({ test: /\.(sa|sc|c)ss$/, + exclude: /vendor-core/i, use: [ { loader: 'style-loader', @@ -205,6 +232,14 @@ const coreConfig = function() { 'sass-loader', ], }); + rules.push({ + test: /vendor-core/i, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + 'sass-loader', + ], + }); config.module.rules = rules; return config; }; @@ -276,8 +311,10 @@ const pluginConfig = function(plugin) { name: pluginName, filename: pluginName + '_remoteEntry.js', exposes: pluginEntries, + shared: shared(true), }) ); + config.plugins = plugins; var rules = config.module.rules; rules.push({ diff --git a/config/webpack.vendor.js b/config/webpack.vendor.js index d6f31b32227..25f29718e8c 100644 --- a/config/webpack.vendor.js +++ b/config/webpack.vendor.js @@ -1 +1,122 @@ -module.exports = ['react-intl', 'intl']; +/* eslint-disable */ + +module.exports = [ + 'intl', + /** + * React related + */ + '@apollo/client', + '@apollo/client/link/batch-http', + '@reduxjs/toolkit', + 'core-js/shim', + 'regenerator-runtime/runtime', + 'formik', + 'rc-input-number', + 'react', + 'react-ace', + 'react-dom', + 'react-dnd', + 'react-dnd-html5-backend', + 'react-debounce-input', + 'react-diff-view', + 'react-ellipsis-with-tooltip', + 'react-onclickoutside', + 'react-password-strength', + 'react-router-dom', + 'react-router-bootstrap', + 'react-loading-skeleton', + 'react-redux', + 'redux', + 'redux-logger', + 'redux-thunk', + 'reselect', + 'prop-types', + 'classnames', + 'seamless-immutable', + 'connected-react-router', + 'react-helmet', + 'react-intl', + + /** + * Patternfly related + */ + 'patternfly-react', + 'patternfly-react-extensions', + '@patternfly/react-core', + '@patternfly/react-icons', + '@patternfly/react-table', + '@patternfly/react-tokens', + '@patternfly/react-styles', + '@patternfly/react-charts', + // '@redhat-cloud-services/frontend-components', + + /** + * ace-builds related + */ + 'ace-builds', + 'ace-builds/src-noconflict/ace', + 'ace-builds/src-noconflict/ext-language_tools', + 'ace-builds/src-noconflict/mode-ruby', + 'ace-builds/src-noconflict/mode-json', + 'ace-builds/src-noconflict/mode-sh', + 'ace-builds/src-noconflict/mode-html_ruby', + 'ace-builds/src-noconflict/mode-xml', + 'ace-builds/src-noconflict/mode-yaml', + 'ace-builds/src-noconflict/theme-github', + 'ace-builds/src-noconflict/theme-monokai', + 'ace-builds/src-noconflict/keybinding-vim', + 'ace-builds/src-noconflict/keybinding-emacs', + 'ace-builds/src-min-noconflict/ext-searchbox', + + /** + * UUID + */ + 'uuid', + 'uuid/v1', + 'uuid/v3', + 'uuid/v4', + 'uuid/v5', + + 'jstz', + 'diff', + /** + * Custom modules + */ + // { + // name: 'jquery', + // path: '@theforeman/vendor-core/lib/customModules/jquery.js', + // }, + // { + // name: 'jstz', + // window: 'jstz', + // }, + // { + // name: 'ipaddr.js', + // window: 'ipaddr', + // }, + // { + // name: 'diff', + // window: 'diff', + // }, + + /** + * Other packages + */ + 'history', + 'number_helpers', + 'lodash', + 'axios', + 'file-saver', + 'humanize-duration', + 'unidiff', + 'urijs', + 'yup', + 'select2', + 'multiselect', + '@novnc/novnc/core/rfb', + '@novnc/novnc', + + // '@spice-project/spice-html5', + // '@webcomponents/webcomponentsjs/webcomponents-bundle', + // '@webcomponents/webcomponentsjs/custom-elements-es5-adapter', +]; diff --git a/package.json b/package.json index 7ae5676471f..efaf2da6a9f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "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": ">=14.0.0 <21.0.0" + "node": ">=14.0.0 <21.0.0", + "npm": ">=8.0.0" }, "scripts": { "lint": "tfm-lint", @@ -20,13 +21,79 @@ "analyze": "./script/webpack-analyze" }, "dependencies": { + "@apollo/client": "^3.3.7", "@module-federation/utilities": "^1.7.0", - "@theforeman/vendor": "^13.1.0", + "@novnc/novnc": "1.3", + "@patternfly/patternfly": "^4.171.1", + "@patternfly/react-charts": "~6.94.15", + "@patternfly/react-core": "~4.271.2", + "@patternfly/react-icons": "~4.93.3", + "@patternfly/react-styles": "~4.92.3", + "@patternfly/react-table": "~4.112.6", + "@patternfly/react-tokens": "~4.94.3", + "@reduxjs/toolkit": "^1.6.0", + "@spice-project/spice-html5": "^0.2.1", + "@webcomponents/webcomponentsjs": "^2.2.10", + "ace-builds": "^1.4.13", + "axios": "^0.21.1", + "bootstrap-sass": "^3.3.7", + "classnames": "^2.2.5", + "connected-react-router": "6.6.1", + "core-js": "^2.5.7", + "datatables.net": "~1.10.12", + "datatables.net-bs": "~1.10.12", + "diff": "^4.0.1", + "dsmorse-gridster": "^0.8.0", + "file-saver": "^2.0.1", + "formik": "^1.5.8", + "graphql": "^15.5.0", "graphql-tag": "^2.11.0", + "history": "^4.7.2", + "humanize-duration": "3.27.0", "intl": "~1.2.5", + "ipaddr.js": "~1.2.0", "jed": "^1.1.1", + "jquery": "~2.2.4", + "jquery-ujs": "~1.2.0", + "jquery.cookie": "~1.4.1", + "jstz": "~1.0.7", + "lodash": "^4.17.14", + "multiselect": "~0.9.12", + "number_helpers": "^0.1.1", "os-browserify": "^0.3.0", - "react-intl": "^2.8.0" + "patternfly": "^3.58.0", + "patternfly-react": "^2.39.17", + "patternfly-react-extensions": "^3.0.12", + "prop-types": "^15.6.0", + "rc-input-number": "^6.0.0", + "react": "^16.9.0", + "react-ace": "^9.5.0", + "react-debounce-input": "^3.2.0", + "react-diff-view": "^2.4.5", + "react-dnd": "^14.0.2", + "react-dnd-html5-backend": "^14.0.0", + "react-dom": "^16.8.1", + "react-ellipsis-with-tooltip": "^1.0.8", + "react-helmet": "^6.1.0", + "react-intl": "^2.8.0", + "react-loading-skeleton": "^1.1.2", + "react-onclickoutside": "^6.6.2", + "react-password-strength": "^2.4.0", + "react-redux": "^7.1.0", + "react-router-bootstrap": "^0.25.0", + "react-router-dom": "^5.1.2", + "redux": "^4.0.4", + "redux-logger": "^2.8.1", + "redux-thunk": "^2.2.0", + "regenerator-runtime": "^0.13.3", + "reselect": "^3.0.1", + "sanitize-html": "2.3.2", + "seamless-immutable": "^7.1.2", + "select2": "~3.5.2-browserify", + "unidiff": "^1.0.0", + "urijs": "^1.19.4", + "uuid": "^3.3.2", + "yup": "^0.29.3" }, "devDependencies": { "@babel/core": "^7.7.0", @@ -47,10 +114,11 @@ "eslint": "^6.7.2", "eslint-plugin-spellcheck": "0.0.17", "graphql": "^15.5.0", + "mini-css-extract-plugin": "^2.9.1", "path-browserify": "^1.0.1", "prettier": "^1.19.1", "pretty-format": "26.6.2", - "react-dnd-test-backend": "^9.4.0", + "react-dnd-test-backend": "^14.0.0", "redux-mock-store": "^1.2.2", "sass": "~1.60.0", "sass-loader": "^13.3.2", @@ -58,11 +126,29 @@ "stylelint": "^9.3.0", "stylelint-config-standard": "^18.0.0", "tabbable": "~5.2.0", - "victory-core": "~36.8.6", - "victory-pie": "~36.8.6", + "victory-core": "36.8.6", + "victory-pie": "36.8.6", "webpack": "^5.75.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^5.0.1", "webpack-stats-plugin": "^1.0.3" + }, + "overrides": { + "victory-core": "36.8.6", + "victory-pie": "36.8.6", + "victory-area": "36.8.6", + "victory-axis": "36.8.6", + "victory-bar": "36.8.6", + "victory-chart": "36.8.6", + "victory-create-container": "36.8.6", + "victory-cursor-container": "36.8.6", + "victory-group": "36.8.6", + "victory-legend": "36.8.6", + "victory-line": "36.8.6", + "victory-scatter": "36.8.6", + "victory-stack": "36.8.6", + "victory-tooltip": "36.8.6", + "victory-voronoi-container": "36.8.6", + "victory-zoom-container": "36.8.6" } } diff --git a/webpack/assets/javascripts/bundle.js b/webpack/assets/javascripts/bundle.js index 397994b337c..045cc5a8969 100644 --- a/webpack/assets/javascripts/bundle.js +++ b/webpack/assets/javascripts/bundle.js @@ -1,5 +1,7 @@ import 'core-js/shim'; import 'regenerator-runtime/runtime'; +import jstz from 'jstz'; +import ipaddr from 'ipaddr.js'; import compute from './foreman_compute_resource'; import componentRegistry from './react_app/components/componentRegistry'; @@ -56,4 +58,6 @@ window.tfm = Object.assign(window.tfm || {}, { componentRegistry, store, lookupKeys, + jstz, + ipaddr, }); diff --git a/webpack/assets/javascripts/exportAll.js b/webpack/assets/javascripts/exportAll.js index d56a6497401..8062a11bbf6 100644 --- a/webpack/assets/javascripts/exportAll.js +++ b/webpack/assets/javascripts/exportAll.js @@ -19,9 +19,11 @@ function generateExports(directoryPath, exportFileContent = '') { !dirent.name.endsWith('.test.js') && !dirent.name.endsWith('.fixtures.js') && !dirent.name.endsWith('mockRequests.js') && + !dirent.name.endsWith('jquery.js') && + !dirent.name.endsWith('jquery.js') && !fileNameWithoutExtension.includes('TestHelper') && !fileNameWithoutExtension.includes('testHelper') && - !fileNameWithoutExtension.includes('APITestSetup') + !fileNameWithoutExtension.includes('vendor-core.scss') ) { let relativeFilePath = path.relative( __dirname, @@ -51,7 +53,8 @@ function generateExports(directoryPath, exportFileContent = '') { const generateExportsFile = () => { let exportFileContent = generateExports(path.join(__dirname, 'react_app')); - exportFileContent = `/* eslint-disable */\n// This file is autogenerated by the webpack/assets/javascripts/exportAll.js script\n// Please do not modify this file directly\n\n${exportFileContent}`; + exportFileContent = `/* eslint-disable */\n// This file is autogenerated by the webpack/assets/javascripts/exportAll.js script\n// Please do not modify this file directly\n\n + console.log('all_react_export');\n\n${exportFileContent}`; fs.writeFileSync( path.join(__dirname, 'all_react_app_exports.js'), diff --git a/webpack/assets/javascripts/jquery.js b/webpack/assets/javascripts/jquery.js new file mode 100644 index 00000000000..be613eadabc --- /dev/null +++ b/webpack/assets/javascripts/jquery.js @@ -0,0 +1,15 @@ +// to avoid webpack alias loop +const jquery = require('../../../node_modules/jquery'); + +window.$ = jquery; +window.jQuery = jquery; +window.jquery = jquery; + +module.exports = jquery; + +require('jquery.cookie'); +require('jquery-ujs'); +require('multiselect'); +require('select2'); +require('datatables.net-bs'); +require('dsmorse-gridster/dist/jquery.dsmorse-gridster'); diff --git a/webpack/assets/javascripts/react_app/Root/ReactApp.js b/webpack/assets/javascripts/react_app/Root/ReactApp.js index 0f3d70fd532..4af38571a2a 100644 --- a/webpack/assets/javascripts/react_app/Root/ReactApp.js +++ b/webpack/assets/javascripts/react_app/Root/ReactApp.js @@ -11,7 +11,6 @@ import apolloClient from './apollo'; import ToastsList from '../components/ToastsList'; import ErrorBoundary from '../components/common/ErrorBoundary'; import ConfirmModal from '../components/ConfirmModal'; -import '../common/scss/vendor-core.scss'; const ReactApp = ({ layout, metadata, toasts }) => { const [context, setContext] = useState({ metadata }); diff --git a/webpack/assets/javascripts/react_app/components/common/forms/OrderableSelect/index.js b/webpack/assets/javascripts/react_app/components/common/forms/OrderableSelect/index.js index 533ab841ae3..fc39637c90b 100644 --- a/webpack/assets/javascripts/react_app/components/common/forms/OrderableSelect/index.js +++ b/webpack/assets/javascripts/react_app/components/common/forms/OrderableSelect/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { DndProvider } from 'react-dnd'; -import HTML5Backend from 'react-dnd-html5-backend'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import OrderableSelect from './OrderableSelect';