Merge pull request #981 from bbassie/fix/static-root-web-root

Make staticRoot include the static/ prefix and use it for plugin bundles
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-08-31 00:38:31 +03:00
commit 0f1fbb8fe5
9 files changed
+102 -7

No files matched your search

+3 -2
View File
@@ -1217,7 +1217,7 @@ export default class Controller {
}
/**
* Servers the web interface with the root path set apropriately
* Serves the web interface with the root path set appropriately
*
* @param route - route the interface is served under.
* @returns Experess.js route handler.
@@ -1231,7 +1231,7 @@ export default class Controller {
return function(req: Request, res: Response, next: NextFunction) {
let depth = routeDepth + Number(req.path.slice(-1) === "/");
let webRoot = "../".repeat(depth) || "./";
let staticRoot = webRoot;
let staticRoot = `${webRoot}static/`;
let mainBundle: string = "";
if (res.app.locals.mainBundle) {
mainBundle = res.app.locals.mainBundle;
@@ -1239,6 +1239,7 @@ export default class Controller {
let stats = res.locals.webpack.devMiddleware.stats.stats[0];
mainBundle = stats.toJson().assetsByChunkName["main"];
}
mainBundle = routes.stripStaticPrefix(mainBundle);
fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => {
res.type("text/html");
+11
View File
@@ -104,6 +104,14 @@ async function getMetrics(req: Request, res: Response, next: any) {
}
/**
* Removes the static/ prefix webpack adds to bundle paths so they can be
* resolved relative to staticRoot.
*/
export function stripStaticPrefix(bundlePath: string) {
return bundlePath.replace(/^static\//, "");
}
function getPlugins(req: Request, res: Response) {
let plugins: lib.PluginWebApi[] = [];
for (let pluginInfo of req.app.locals.controller.pluginInfos) {
@@ -129,6 +137,9 @@ function getPlugins(req: Request, res: Response) {
if (web.main === "remoteEntry.js") {
web.error = "Incompatible old remoteEntry.js entrypoint.";
}
if (web.main) {
web.main = stripStaticPrefix(web.main);
}
plugins.push({ name, version: pluginInfo.version, enabled, loaded, web, npmPackage: pluginInfo.npmPackage });
}
res.send(plugins);
+2
View File
@@ -13,7 +13,9 @@ declare module "*.md" {
export default value;
}
/** Path the web interface is served under, ends with a slash. */
declare const webRoot: string;
/** Path static assets are served under, ends with a slash. */
declare const staticRoot: string;
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
+1 -1
View File
@@ -50,7 +50,7 @@ async function loadPluginInfos(): Promise<[lib.PluginWebpackEnvInfo[], string]>
continue;
}
try {
await loadScript(`${webRoot}${meta.web.main}`);
await loadScript(`${staticRoot}${meta.web.main}`);
let container: any = (window as { [key: string]: any })[`plugin_${meta.name}`];
if (!container) {
throw new Error(`Plugin did not expose its container via plugin_${meta.name}`);
@@ -1102,7 +1102,7 @@ function useExportedAsset(modPack: lib.ModPack | undefined, asset: "settings"|"l
return;
}
let response = await fetch(`${staticRoot}static/${assetFilename}`);
let response = await fetch(`${staticRoot}${assetFilename}`);
if (response.ok) {
let data = await response.json();
setAssetData(asset === "locale" ? new Map(data) : data);
@@ -21,7 +21,7 @@ class ExportLocaleStore {
return;
}
let response = await fetch(`${staticRoot}static/${cacheEntry.localePath}`);
let response = await fetch(`${staticRoot}${cacheEntry.localePath}`);
if (response.ok) {
cacheEntry.data = new Map(await response.json() as ExportLocale);
for (const callback of cacheEntry.callbacks) {
@@ -26,7 +26,7 @@ class ExportPrototypeMetadataStore {
return;
}
const response = await fetch(`${staticRoot}static/${cacheEntry.metadataPath}`);
const response = await fetch(`${staticRoot}${cacheEntry.metadataPath}`);
if (!response.ok) {
notify(
`Failed to fetch prototype metadata for mod pack ${modPackId}, server returned: ` +
@@ -43,7 +43,7 @@ class ExportPrototypeMetadataStore {
const metadata = await response.json() as ExportMetadata;
const spriteUrl = `${staticRoot}static/${cacheEntry.spritesheetPath}`;
const spriteUrl = `${staticRoot}${cacheEntry.spritesheetPath}`;
const styleContent: string[] = [];
styleContent.push(`\
.factorio-icon\
+37
View File
@@ -3,6 +3,9 @@ const path = require("node:path");
const assert = require("assert").strict;
const { Controller, HostRecord, InstanceRecord, UserRecord } = require("@clusterio/controller");
const { EventEmitter } = require("stream");
const events = require("node:events");
const http = require("node:http");
const express = require("express");
const {
ControllerConfig, Address, RequestError,
@@ -48,6 +51,40 @@ describe("controller/src/Controller", function() {
assert(list.check(ip), `${ip} missing from list`);
}
}
describe(".serveWeb()", function() {
let server, port;
before(async function() {
const app = express();
app.locals.controller = { config: { get: () => "Test Cluster" } };
app.locals.mainBundle = "static/main.abc.js";
for (let route of ["/", "/instances/:id/view"]) {
app.get(route, Controller.serveWeb(route));
}
server = http.createServer(app);
server.listen(0, "localhost");
await events.once(server, "listening");
port = server.address().port;
});
after(function() {
server.close();
});
it("should serve the root with static/ in staticRoot", async function() {
const html = await (await fetch(`http://localhost:${port}/`)).text();
assert(html.includes("<title>Test Cluster</title>"));
assert(html.includes("src=\"./static/main.abc.js\""));
assert(html.includes("new URL(\"./\", document.location)"));
assert(html.includes("new URL(\"./static/\", document.location)"));
});
it("should use relative roots for nested routes", async function() {
let html = await (await fetch(`http://localhost:${port}/instances/1/view`)).text();
assert(html.includes("src=\"../../static/main.abc.js\""));
assert(html.includes("new URL(\"../../\", document.location)"));
assert(html.includes("new URL(\"../../static/\", document.location)"));
html = await (await fetch(`http://localhost:${port}/instances/1/view/`)).text();
assert(html.includes("src=\"../../../static/main.abc.js\""));
assert(html.includes("new URL(\"../../../static/\", document.location)"));
});
});
describe(".parseTrustedProxies()", function() {
it("should parse addresses", function() {
controller.config.set("controller.trusted_proxies", "10.0.0.1");
+44
View File
@@ -73,6 +73,50 @@ describe("controller/src/routes", function() {
assert.equal(await responses[1].text(), "test content");
});
});
describe("stripStaticPrefix()", function() {
it("should remove a leading static/", function() {
assert.equal(routes.stripStaticPrefix("static/main.abc.js"), "main.abc.js");
});
it("should leave other paths alone", function() {
assert.equal(routes.stripStaticPrefix("main.abc.js"), "main.abc.js");
assert.equal(routes.stripStaticPrefix("other/static/main.js"), "other/static/main.js");
});
});
describe("/api/plugins", function() {
let endpoint;
beforeEach(function() {
endpoint = `http://localhost:${port}/api/plugins`;
controller.plugins = new Map([["foo", {}]]);
controller.mockConfigEntries.set("foo.load_plugin", true);
});
it("should strip static/ from the plugin bundle path", async function() {
controller.pluginInfos = [{
name: "foo", version: "1.0.0", npmPackage: "foo",
manifest: { "foo.js": "static/foo.abc.js" },
}];
let response = await fetch(endpoint);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), [{
name: "foo", version: "1.0.0", enabled: true, loaded: true, npmPackage: "foo",
web: { main: "foo.abc.js" },
}]);
});
it("should report missing manifest and entrypoint", async function() {
controller.pluginInfos = [
{ name: "foo", version: "1.0.0", npmPackage: "foo" },
{ name: "bar", version: "1.0.0", npmPackage: "bar", manifest: {} },
{ name: "baz", version: "1.0.0", npmPackage: "baz", manifest: { "baz.js": "remoteEntry.js" } },
];
let response = await fetch(endpoint);
assert.equal(response.status, 200);
let data = await response.json();
assert.equal(data[0].web.error, "Missing dist/web/manifest.json");
assert.equal(data[1].web.error, "Missing bar.js entry in manifest.json");
assert.equal(data[2].web.error, "Incompatible old remoteEntry.js entrypoint.");
assert.equal(data[1].loaded, false);
assert.equal(data[1].enabled, false);
});
});
describe("/api/cluster-name", function() {
let endpoint;
beforeEach(function() {