From 6dbf29153eb2f3d3d369db31fcc45274d548d014 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 24 Oct 2020 14:03:14 +0200 Subject: [PATCH] Implement plugin loading in web interface Add loading of plugin info to the web interface via webpack Module Federation. --- .eslintignore | 1 + .gitignore | 1 + package.json | 3 +- packages/master/master.js | 15 ++++ packages/master/package.json | 8 +- packages/master/web/basename.jsx | 2 +- packages/master/web/bootstrap.jsx | 77 +++++++++++++++++++ packages/master/web/index.html | 2 +- packages/master/web/index.jsx | 9 +-- .../master/web/pages/settings/settings.jsx | 13 ---- packages/master/web/util/wslink.jsx | 20 +---- packages/master/webpack.common.js | 71 +++++++++++++++++ packages/master/webpack.config.js | 68 ++++------------ plugins/global_chat/package.json | 13 ++++ plugins/global_chat/web/index.jsx | 0 plugins/global_chat/webpack.config.js | 28 +++++++ plugins/research_sync/package.json | 15 +++- plugins/research_sync/web/index.jsx | 0 plugins/research_sync/webpack.config.js | 28 +++++++ plugins/statistics_exporter/package.json | 13 ++++ plugins/statistics_exporter/web/index.jsx | 0 plugins/statistics_exporter/webpack.config.js | 28 +++++++ plugins/subspace_storage/package.json | 16 ++++ plugins/subspace_storage/web/index.jsx | 0 plugins/subspace_storage/webpack.config.js | 31 ++++++++ 25 files changed, 360 insertions(+), 102 deletions(-) create mode 100644 packages/master/web/bootstrap.jsx create mode 100644 packages/master/webpack.common.js create mode 100644 plugins/global_chat/web/index.jsx create mode 100644 plugins/global_chat/webpack.config.js create mode 100644 plugins/research_sync/web/index.jsx create mode 100644 plugins/research_sync/webpack.config.js create mode 100644 plugins/statistics_exporter/web/index.jsx create mode 100644 plugins/statistics_exporter/webpack.config.js create mode 100644 plugins/subspace_storage/web/index.jsx create mode 100644 plugins/subspace_storage/webpack.config.js diff --git a/.eslintignore b/.eslintignore index 444b47c8..d3cfadef 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ packages/master/static +plugins/*/static diff --git a/.gitignore b/.gitignore index f3818817..4fc2dc3a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ saves # Web build /packages/master/static +/plugins/*/static # IDE .idea/ diff --git a/package.json b/package.json index efdf2697..22cd7e21 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "lerna": "^3.22.1", "mocha": "^6.2.0", "nyc": "^14.1.1", - "phin": "^3.4.1" + "phin": "^3.4.1", + "webpack": "^5.0.0" } } diff --git a/packages/master/master.js b/packages/master/master.js index b1c866f8..b8adf2fd 100755 --- a/packages/master/master.js +++ b/packages/master/master.js @@ -201,6 +201,16 @@ function validateSlaveToken(req, res, next) { next(); } +app.get("/api/plugins", (req, res) => { + let plugins = []; + for (let pluginInfo of pluginInfos) { + let name = pluginInfo.name; + let enabled = masterPlugins.has(name) && masterConfig.group(name).get("enabled"); + plugins.push({ name, enabled }); + } + res.send(plugins); +}); + // Handle an uploaded export package. async function uploadExport(req, res, next) { endpointHitCounter.labels(req.route.path).inc(); @@ -1744,6 +1754,11 @@ async function startServer() { for (let route of pluginInfo.routes || []) { app.get(route, serveWeb(route)); } + + let pluginPackagePath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json")); + let staticPath = path.join(path.dirname(pluginPackagePath), "static"); + console.log(`/plugin/${pluginInfo.name}`, staticPath); + app.use(`/plugin/${pluginInfo.name}`, express.static(staticPath)); } // Load plugins diff --git a/packages/master/package.json b/packages/master/package.json index 1289c6c4..0db50627 100644 --- a/packages/master/package.json +++ b/packages/master/package.json @@ -6,7 +6,7 @@ "version": "2.0.0-alpha.0", "main": "master.js", "scripts": { - "build": "webpack --env.production", + "build": "webpack-cli --env.production", "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { @@ -42,15 +42,17 @@ "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", "css-loader": "^4.3.0", + "events": "^3.2.0", "fast-deep-equal": "^3.1.3", "null-loader": "^4.0.0", + "path-browserify": "^1.0.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", "style-loader": "^1.2.1", - "webpack": "^4.44.2", "webpack-cli": "^3.3.12", - "webpack-dev-middleware": "^3.7.2" + "webpack-dev-middleware": "^3.7.2", + "webpack-merge": "^5.2.0" }, "publishConfig": { "access": "public" diff --git a/packages/master/web/basename.jsx b/packages/master/web/basename.jsx index a44e5ff7..0e7e3de4 100644 --- a/packages/master/web/basename.jsx +++ b/packages/master/web/basename.jsx @@ -1,6 +1,6 @@ // Calculate basedir of application based on current location and // the provided web_root from master server. -const location = new URL(window.web_root, document.location); +const location = new URL(window.webRoot, document.location); const basename = location.pathname !== "/" ? location.pathname : ""; export default basename; diff --git a/packages/master/web/bootstrap.jsx b/packages/master/web/bootstrap.jsx new file mode 100644 index 00000000..f534ccac --- /dev/null +++ b/packages/master/web/bootstrap.jsx @@ -0,0 +1,77 @@ +import "./index.css"; + +import React from "react"; +import ReactDOM from "react-dom"; + +import libConfig from "@clusterio/lib/config"; + +import App from "./components/App"; +import { Control, ControlConnector } from "./util/websocket"; + +async function loadScript(url) { + let script = document.createElement("script"); + script.src = url; + script.type = "text/javascript"; + script.async = true; + + let result = new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = reject; + }); + + document.head.appendChild(script); + return result; +} + +async function loadPluginInfos() { + let response = await fetch(`${webRoot}api/plugins`); + let pluginList; + if (response.ok) { + pluginList = await response.json(); + + } else { + console.log("Failed to get plugin data, running without plugins"); + pluginList = []; + } + + let pluginInfos = []; + await __webpack_init_sharing__("default"); + for (let plugin of pluginList) { + try { + await loadScript(`${webRoot}plugin/${plugin.name}/remoteEntry.js`); + let container = window[`plugin_${plugin.name}`]; + await container.init(__webpack_share_scopes__.default); + let info = (await container.get("./info"))(); + info.container = container; + pluginInfos.push(info); + + } catch (err) { + console.log(`Failed to load plugin info for ${plugin.name}`); + console.log(err); + } + } + return pluginInfos; +} + +async function load() { + let pluginInfos = await loadPluginInfos(); + libConfig.registerPluginConfigGroups(pluginInfos); + libConfig.finalizeConfigs(); + + let wsUrl = new URL(window.webRoot, document.location); + wsUrl.protocol = wsUrl.protocol.replace("http", "ws"); + + let controlConnector = new ControlConnector(wsUrl, 10, localStorage.getItem("master_token")); + // XXX come up with a better way of sharing this + window.control = new Control(controlConnector, []); + try { + await controlConnector.connect(); + } catch (err) { + // XXX: What now? + console.log(err); + } + + ReactDOM.render(, document.getElementById("root")); +} + +load().catch(console.log); diff --git a/packages/master/web/index.html b/packages/master/web/index.html index b7a280f7..61d74ee8 100644 --- a/packages/master/web/index.html +++ b/packages/master/web/index.html @@ -5,7 +5,7 @@ Clusterio diff --git a/packages/master/web/index.jsx b/packages/master/web/index.jsx index 5e08dc22..50599f7a 100644 --- a/packages/master/web/index.jsx +++ b/packages/master/web/index.jsx @@ -1,8 +1 @@ -import "./index.css"; - -import React from "react"; -import ReactDOM from "react-dom"; - -import App from "./components/App"; - -ReactDOM.render(, document.getElementById("root")); +import("./bootstrap"); diff --git a/packages/master/web/pages/settings/settings.jsx b/packages/master/web/pages/settings/settings.jsx index 2b1460b7..036884ba 100644 --- a/packages/master/web/pages/settings/settings.jsx +++ b/packages/master/web/pages/settings/settings.jsx @@ -36,7 +36,6 @@ export class Settings extends Component { {...layout} name="basic" initialValues={{ - master_url: localStorage.getItem("master_url"), master_token: localStorage.getItem("master_token"), }} onFinish={values => { @@ -48,18 +47,6 @@ export class Settings extends Component { document.location = document.location; }} > - - - { - config.finalizeConfigs(); - controlConnector = new ControlConnector( - localStorage.getItem("master_url"), - 10, // Reconnect delay - localStorage.getItem("master_token"), - ); - control = new Control(controlConnector, []); - try { - await controlConnector.connect(); - } catch (err) { - console.log(err); - } -})(); +// Note: control is created as in bootstrap.jsx as a global. let listSlaves = createListFunction("listSlaves"); let listInstances = createListFunction("listInstances"); diff --git a/packages/master/webpack.common.js b/packages/master/webpack.common.js new file mode 100644 index 00000000..7b3bdb01 --- /dev/null +++ b/packages/master/webpack.common.js @@ -0,0 +1,71 @@ +"use strict"; +const webpack = require("webpack"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); + +module.exports = (env = {}) => ({ + mode: env.production ? "production" : "development", + devtool: env.production ? "source-map" : "eval-source-map", + performance: { + maxAssetSize: 2**21, + maxEntrypointSize: 2**21, + }, + plugins: [ + new CleanWebpackPlugin(), + new webpack.DefinePlugin({ + "process.env.APP_ENV": JSON.stringify("browser"), + }), + + // Make sure ant-design icons use the ES variant + new webpack.NormalModuleReplacementPlugin( + /@ant-design\/icons\/[A-Z]/, + function(resource) { + resource.request = resource.request.replace(/@ant-design\/icons/, "$&/es/icons"); + } + ), + ], + module: { + rules: [ + { + test: /node_modules.fs-extra/, + use: "null-loader", + }, + { + test: /\.css$/, + use: [ + "style-loader", + { + loader: "css-loader", + options: { + sourceMap: !env.production, + }, + }, + ], + }, + { + test: /\.js$/, + resolve: { + // XXX Required due to bug in babel-runtime + // see: https://github.com/babel/babel/issues/12058 + fullySpecified: false, + }, + }, + { + test: /\.jsx$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-react"], + }, + }, + }, + ], + }, + resolve: { + extensions: [".js", ".json", ".jsx"], + fallback: { + "events": require.resolve("events/"), + "path": require.resolve("path-browserify"), + }, + }, +}); diff --git a/packages/master/webpack.config.js b/packages/master/webpack.config.js index 0232ea94..6ed48d69 100644 --- a/packages/master/webpack.config.js +++ b/packages/master/webpack.config.js @@ -1,12 +1,12 @@ "use strict"; const path = require("path"); const webpack = require("webpack"); -const { CleanWebpackPlugin } = require("clean-webpack-plugin"); +const { merge } = require("webpack-merge"); -module.exports = (env = {}) => ({ +const common = require("./webpack.common"); + +module.exports = (env = {}) => merge(common(env), { entry: "./web/index.jsx", - mode: env.production ? "production" : "development", - devtool: env.production ? "source-map" : "eval-source-map", devServer: { contentBase: "./static", }, @@ -14,58 +14,16 @@ module.exports = (env = {}) => ({ filename: "bundle.js", path: path.resolve(__dirname, "static"), }, - performance: { - maxAssetSize: 2**21, - maxEntrypointSize: 2**21, - }, plugins: [ - new CleanWebpackPlugin(), - new webpack.DefinePlugin({ - "process.env.APP_ENV": JSON.stringify("browser"), + new webpack.container.ModuleFederationPlugin({ + name: "master", + shared: { + "@clusterio/lib": { singleton: true }, + "ajv": {}, + "antd": { singleton: true }, + "react": { singleton: true }, + "react-dom": { singleton: true }, + }, }), - - // Make sure ant-design icons use the ES variant - new webpack.NormalModuleReplacementPlugin( - /@ant-design\/icons\/[A-Z]/, - function(resource) { - resource.request = resource.request.replace(/@ant-design\/icons/, "$&/es/icons"); - } - ), ], - module: { - rules: [ - { - test: /node_modules.fs-extra/, - use: "null-loader", - }, - { - test: /\.css$/, - use: [ - "style-loader", - { - loader: "css-loader", - options: { - sourceMap: !env.production, - }, - }, - ], - }, - { - test: /\.jsx$/, - exclude: /node_modules/, - use: { - loader: "babel-loader", - options: { - presets: ["@babel/preset-react"], - }, - }, - }, - ], - }, - resolve: { - extensions: [".js", ".json", ".jsx"], - }, - node: { - "crypto": "empty", - }, }); diff --git a/plugins/global_chat/package.json b/plugins/global_chat/package.json index 836797e7..4a3c1843 100644 --- a/plugins/global_chat/package.json +++ b/plugins/global_chat/package.json @@ -7,6 +7,7 @@ "license": "MIT", "repository": "clusterio/factorioClusterio", "scripts": { + "build": "webpack-cli --env.production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -15,6 +16,18 @@ "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" }, + "devDependencies": { + "@babel/core": "^7.11.6", + "@babel/preset-react": "^7.10.4", + "@clusterio/master": "^2.0.0-alpha.0", + "babel-loader": "^8.1.0", + "clean-webpack-plugin": "^3.0.0", + "events": "^3.2.0", + "null-loader": "^4.0.0", + "path-browserify": "^1.0.1", + "webpack-cli": "^3.3.12", + "webpack-merge": "^5.2.0" + }, "publishConfig": { "access": "public" } diff --git a/plugins/global_chat/web/index.jsx b/plugins/global_chat/web/index.jsx new file mode 100644 index 00000000..e69de29b diff --git a/plugins/global_chat/webpack.config.js b/plugins/global_chat/webpack.config.js new file mode 100644 index 00000000..f338be0e --- /dev/null +++ b/plugins/global_chat/webpack.config.js @@ -0,0 +1,28 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/master/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + entry: "./web/index.jsx", + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "static"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "global_chat", + library: { type: "var", name: "plugin_global_chat" }, + filename: "remoteEntry.js", + exposes: { + "./info": "./info.js", + }, + shared: { + "@clusterio/lib": { import: false }, + "ajv": { import: false }, + }, + }), + ], +}); diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index fba7b504..8e7fc793 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -7,6 +7,7 @@ "license": "MIT", "repository": "clusterio/factorioClusterio", "scripts": { + "build": "webpack-cli --env.production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -15,9 +16,21 @@ "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" }, - "ependencies": { + "dependencies": { "fs-extra": "^8.1.0" }, + "devDependencies": { + "@babel/core": "^7.11.6", + "@babel/preset-react": "^7.10.4", + "@clusterio/master": "^2.0.0-alpha.0", + "babel-loader": "^8.1.0", + "clean-webpack-plugin": "^3.0.0", + "events": "^3.2.0", + "null-loader": "^4.0.0", + "path-browserify": "^1.0.1", + "webpack-cli": "^3.3.12", + "webpack-merge": "^5.2.0" + }, "publishConfig": { "access": "public" } diff --git a/plugins/research_sync/web/index.jsx b/plugins/research_sync/web/index.jsx new file mode 100644 index 00000000..e69de29b diff --git a/plugins/research_sync/webpack.config.js b/plugins/research_sync/webpack.config.js new file mode 100644 index 00000000..a3151f33 --- /dev/null +++ b/plugins/research_sync/webpack.config.js @@ -0,0 +1,28 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/master/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + entry: "./web/index.jsx", + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "static"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "research_sync", + library: { type: "var", name: "plugin_research_sync" }, + filename: "remoteEntry.js", + exposes: { + "./info": "./info.js", + }, + shared: { + "@clusterio/lib": { import: false }, + "ajv": { import: false }, + }, + }), + ], +}); diff --git a/plugins/statistics_exporter/package.json b/plugins/statistics_exporter/package.json index 2a0a8266..b338e643 100644 --- a/plugins/statistics_exporter/package.json +++ b/plugins/statistics_exporter/package.json @@ -7,6 +7,7 @@ "license": "MIT", "repository": "clusterio/factorioClusterio", "scripts": { + "build": "webpack-cli --env.production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -15,6 +16,18 @@ "peerDependencies": { "@clusterio/lib": "^2.0.0-alpha.0" }, + "devDependencies": { + "@babel/core": "^7.11.6", + "@babel/preset-react": "^7.10.4", + "@clusterio/master": "^2.0.0-alpha.0", + "babel-loader": "^8.1.0", + "clean-webpack-plugin": "^3.0.0", + "events": "^3.2.0", + "null-loader": "^4.0.0", + "path-browserify": "^1.0.1", + "webpack-cli": "^3.3.12", + "webpack-merge": "^5.2.0" + }, "publishConfig": { "access": "public" } diff --git a/plugins/statistics_exporter/web/index.jsx b/plugins/statistics_exporter/web/index.jsx new file mode 100644 index 00000000..e69de29b diff --git a/plugins/statistics_exporter/webpack.config.js b/plugins/statistics_exporter/webpack.config.js new file mode 100644 index 00000000..129dcf0f --- /dev/null +++ b/plugins/statistics_exporter/webpack.config.js @@ -0,0 +1,28 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/master/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + entry: "./web/index.jsx", + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "static"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "statistics_exporter", + library: { type: "var", name: "plugin_statistics_exporter" }, + filename: "remoteEntry.js", + exposes: { + "./info": "./info.js", + }, + shared: { + "@clusterio/lib": { import: false }, + "ajv": { import: false }, + }, + }), + ], +}); diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index fc6aa989..a78f83d2 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -7,6 +7,7 @@ "license": "MIT", "repository": "clusterio/factorioClusterio", "scripts": { + "build": "webpack-cli --env.production", "test": "echo \"Error: run tests from root\" && exit 1" }, "bugs": { @@ -18,6 +19,21 @@ "dependencies": { "fs-extra": "^8.1.0" }, + "devDependencies": { + "@babel/core": "^7.11.6", + "@babel/preset-react": "^7.10.4", + "@clusterio/master": "^2.0.0-alpha.0", + "antd": "^4.6.5", + "babel-loader": "^8.1.0", + "clean-webpack-plugin": "^3.0.0", + "events": "^3.2.0", + "null-loader": "^4.0.0", + "path-browserify": "^1.0.1", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "webpack-cli": "^3.3.12", + "webpack-merge": "^5.2.0" + }, "publishConfig": { "access": "public" } diff --git a/plugins/subspace_storage/web/index.jsx b/plugins/subspace_storage/web/index.jsx new file mode 100644 index 00000000..e69de29b diff --git a/plugins/subspace_storage/webpack.config.js b/plugins/subspace_storage/webpack.config.js new file mode 100644 index 00000000..05814382 --- /dev/null +++ b/plugins/subspace_storage/webpack.config.js @@ -0,0 +1,31 @@ +"use strict"; +const path = require("path"); +const webpack = require("webpack"); +const { merge } = require("webpack-merge"); + +const common = require("@clusterio/master/webpack.common"); + +module.exports = (env = {}) => merge(common(env), { + entry: "./web/index.jsx", + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "static"), + }, + plugins: [ + new webpack.container.ModuleFederationPlugin({ + name: "subspace_storage", + library: { type: "var", name: "plugin_subspace_storage" }, + filename: "remoteEntry.js", + exposes: { + "./info": "./info.js", + }, + shared: { + "@clusterio/lib": { import: false }, + "ajv": { import: false }, + "antd": { import: false }, + "react": { import: false }, + "react-dom": { import: false }, + }, + }), + ], +});