Implement plugin loading in web interface

Add loading of plugin info to the web interface via webpack Module
Federation.
This commit is contained in:
Hornwitser committed 2020-10-27 18:24:45 +01:00
1 parent 542d8a798f
commit 6dbf29153e
25 files changed
+360 -102

No files matched your search

+1
View File
@@ -1 +1,2 @@
packages/master/static
plugins/*/static
+1
View File
@@ -47,6 +47,7 @@ saves
# Web build
/packages/master/static
/plugins/*/static
# IDE
.idea/
+2 -1
View File
@@ -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"
}
}
+15
View File
@@ -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
+5 -3
View File
@@ -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"
+1 -1
View File
@@ -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;
+77
View File
@@ -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(<App />, document.getElementById("root"));
}
load().catch(console.log);
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Clusterio</title>
<script>
window.web_root = "__WEB_ROOT__";
window.webRoot = "__WEB_ROOT__";
</script>
</head>
<body>
+1 -8
View File
@@ -1,8 +1 @@
import "./index.css";
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
ReactDOM.render(<App />, document.getElementById("root"));
import("./bootstrap");
@@ -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;
}}
>
<Form.Item
label="Master URL"
name="master_url"
rules={[
{
required: true,
message: "Please input master url, ex ws://localhost:8080 or wss://localhost:8443/ . For SSL connections, ensure you have a valid certificate.",
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="User auth token"
name="master_token"
+1 -19
View File
@@ -1,25 +1,7 @@
import { Control, ControlConnector } from "./../util/websocket";
import * as errors from "@clusterio/lib/errors";
import * as link from "@clusterio/lib/link";
import config from "@clusterio/lib/config";
let control, controlConnector;
// Create WS connection to master. Used by all components through the interface exported here.
(async () => {
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");
+71
View File
@@ -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"),
},
},
});
+13 -55
View File
@@ -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",
},
});
+13
View File
@@ -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"
}
View File
Whitespace-only changes.
+28
View File
@@ -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 },
},
}),
],
});
+14 -1
View File
@@ -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"
}
View File
Whitespace-only changes.
+28
View File
@@ -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 },
},
}),
],
});
+13
View File
@@ -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"
}
Whitespace-only changes.
@@ -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 },
},
}),
],
});
+16
View File
@@ -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"
}
Whitespace-only changes.
@@ -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 },
},
}),
],
});