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 },
+ },
+ }),
+ ],
+});