diff --git a/.eslintrc b/.eslintrc index 992dfe343fa3..0de7f9bd00b2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,6 +23,7 @@ "ascii", "auditable", "Autocompletion", + "autogenerated", "bool", "bootable", "Borderless", diff --git a/.gitignore b/.gitignore index 09cd7cbb0b27..a0034dcfa118 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,5 @@ npm-debug.log .solargraph.yml .nvmrc mkmf.log -.yardoc/ \ No newline at end of file +.yardoc/ +webpack/assets/javascripts/all_react_app_exports.js \ No newline at end of file diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 0374c02a3a4c..63d70227bb5a 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -39,6 +39,7 @@ <%= get_webpack_foreman_vendor_js %> <%= javascript_include_tag('/webpack/vendor.js') %> <%= javascript_include_tag('/webpack/bundle.js') %> + <%= javascript_include_tag('/webpack/reactExports.js') %> <%= javascript_include_tag 'application' %> <%= webpacked_plugins_with_global_js %> diff --git a/config/webpack.config.js b/config/webpack.config.js index 7c4d73eeeee4..f280a98809df 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -12,6 +12,7 @@ var vendorEntry = require('./webpack.vendor'); var fs = require('fs'); const { ModuleFederationPlugin } = require('webpack').container; var pluginUtils = require('../script/plugin_webpack_directories'); +var { generateExportsFile }= require('../webpack/assets/javascripts/exportAll'); class AddRuntimeRequirement { // to avoid "webpackRequire.l is not a function" error @@ -153,10 +154,19 @@ const coreConfig = function() { config.entry = { bundle: { import: bundleEntry, dependOn: 'vendor' }, vendor: vendorEntry, + reactExports: path.join( + __dirname, + '..', + 'webpack/assets/javascripts/all_react_app_exports.js' + ), }; config.output = { path: path.join(__dirname, '..', 'public', 'webpack'), publicPath: '/webpack/', + library: { + name: ['TheForeman', '[id]'], + type: 'var', + }, }; var plugins = config.plugins; @@ -196,6 +206,21 @@ const pluginConfig = function(plugin) { var config = commonConfig(); config.context = path.join(pluginRoot, 'webpack'); config.entry = {}; + + function convertImportStatement(importStatement) { + const importPath = importStatement; + const importPathParts = importPath.split('/'); + const newImportName = importPathParts.slice(1).join('_'); + return newImportName; + } + config.externals = function({ request }, callback) { + if (/^foremanReact(\/.*)?$/.test(request)) { + const prefix = 'var TheForeman.reactExports.'; + const newPath = prefix + convertImportStatement(request.substring('foremanReact'.length)); + return callback(null, newPath); + } + return callback(); + }; var pluginEntries = { './index': path.resolve(pluginRoot, 'webpack', 'index'), }; @@ -233,6 +258,7 @@ const pluginConfig = function(plugin) { //get the list of webpack plugins var plugins = config.plugins; + plugins.push( new ModuleFederationPlugin({ name: pluginName, @@ -262,6 +288,7 @@ const pluginConfig = function(plugin) { }; module.exports = function(env, argv) { + generateExportsFile(); const { pluginName } = env; var pluginsDirs = pluginUtils.getPluginDirs('pipe'); var pluginsInfo = {}; diff --git a/webpack/assets/javascripts/exportAll.js b/webpack/assets/javascripts/exportAll.js new file mode 100644 index 000000000000..f1efea65b652 --- /dev/null +++ b/webpack/assets/javascripts/exportAll.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); + +function generateExports(directoryPath, exportFileContent = '') { + fs.readdirSync(directoryPath, { withFileTypes: true }).forEach(dirent => { + if ( + dirent.isDirectory() && + dirent.name !== '__mocks__' && + dirent.name !== '__tests__' + ) { + const subDirectoryPath = path.join(directoryPath, dirent.name); + exportFileContent = generateExports(subDirectoryPath, exportFileContent); + } else if (dirent.isFile()) { + const fileNameWithoutExtension = path.parse(dirent.name).name; + const fileExtension = path.parse(dirent.name).ext; + + if ( + fileExtension === '.js' && + !dirent.name.endsWith('.test.js') && + !dirent.name.endsWith('.fixtures.js') && + !fileNameWithoutExtension.includes('TestHelper') && + !fileNameWithoutExtension.includes('testHelper') && + !fileNameWithoutExtension.includes('APITestSetup') + ) { + let relativeFilePath = path.relative( + __dirname, + path.join(directoryPath, fileNameWithoutExtension) + ); + relativeFilePath = relativeFilePath.substring( + relativeFilePath.indexOf('react_app') + 'react_app'.length + 1 + ); + let fileName = relativeFilePath.replace(/\\/g, '/').replace(/\//g, '_'); // replace slashes with underscores + fileName = fileName.replace(/\./g, ''); // remove dots + + if (fileName.endsWith('_index')) { + fileName = fileName.substring(0, fileName.length - '_index'.length); // remove _index + } + + exportFileContent += `import * as ${fileName} from './react_app/${relativeFilePath.replace( + /\\/g, + '/' + )}';\n`; + exportFileContent += `export { ${fileName} };\n`; + } + } + }); + + return 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}`; + + fs.writeFileSync( + path.join(__dirname, 'all_react_app_exports.js'), + exportFileContent + ); +}; + +module.exports = { generateExportsFile }; diff --git a/webpack/assets/javascripts/react_app/mockRequests.js b/webpack/assets/javascripts/react_app/mockRequests.js index 156866745ea0..954b7cb85184 100644 --- a/webpack/assets/javascripts/react_app/mockRequests.js +++ b/webpack/assets/javascripts/react_app/mockRequests.js @@ -1,7 +1,7 @@ import axios from 'axios'; import { MockAdapter } from '@theforeman/test'; -export const mock = new MockAdapter(axios); +export const mock = () => new MockAdapter(axios); const methods = { GET: 'onGet', POST: 'onPost', @@ -15,6 +15,9 @@ export const mockRequest = ({ data = null, status = 200, response = null, -}) => mock[methods[method]](url, data).reply(status, response); +}) => + mock() + [methods[method]](url, data) + .reply(status, response); -export const mockReset = () => mock.reset(); +export const mockReset = () => mock().reset();