Make staticRoot include the static/ prefix and use it for plugin bundles

staticRoot was set to the same value as webRoot, so the static/ prefix
ended up scattered across consumers: index.html relied on the manifest
path containing it, the export stores hardcoded `${staticRoot}static/`,
and bootstrap loaded plugin bundles from webRoot instead. Now staticRoot
is `${webRoot}static/` and the controller strips the static/ prefix from
bundle paths before handing them to the page and to /api/plugins.

Plugins that fetch assets with `${staticRoot}static/...` need to drop the
static/ part.

Fixes #980

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Fable 5 committed 2026-08-28 09:50:08 +00:00
1 parent 59cd193b65
commit 2f4e0c4418
8 files changed
+31 -7

No files matched your search

+10
View File
@@ -31,6 +31,16 @@ Many thanks to the following for contributing to this release:
[@username](https://github.com/username) [@username](https://github.com/username)
--> -->
## Version 2.0.0-alpha.28
### Fixes
- Plugin web bundles are now loaded relative to `staticRoot` instead of `webRoot`. [#980](<https://github.com/clusterio/clusterio/issues/980>)
### Breaking Changes
- `staticRoot` in the web interface now includes the `static/` prefix. Plugins that fetch assets with `${staticRoot}static/...` should drop the `static/` part. [#980](<https://github.com/clusterio/clusterio/issues/980>)
## Version 2.0.0-alpha.27 ## Version 2.0.0-alpha.27
### Features ### Features
+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. * @param route - route the interface is served under.
* @returns Experess.js route handler. * @returns Experess.js route handler.
@@ -1231,7 +1231,7 @@ export default class Controller {
return function(req: Request, res: Response, next: NextFunction) { return function(req: Request, res: Response, next: NextFunction) {
let depth = routeDepth + Number(req.path.slice(-1) === "/"); let depth = routeDepth + Number(req.path.slice(-1) === "/");
let webRoot = "../".repeat(depth) || "./"; let webRoot = "../".repeat(depth) || "./";
let staticRoot = webRoot; let staticRoot = `${webRoot}static/`;
let mainBundle: string = ""; let mainBundle: string = "";
if (res.app.locals.mainBundle) { if (res.app.locals.mainBundle) {
mainBundle = 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]; let stats = res.locals.webpack.devMiddleware.stats.stats[0];
mainBundle = stats.toJson().assetsByChunkName["main"]; mainBundle = stats.toJson().assetsByChunkName["main"];
} }
mainBundle = routes.stripStaticPrefix(mainBundle);
fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => { fs.readFile(path.join(__dirname, "..", "..", "..", "web", "index.html"), "utf8").then((content) => {
res.type("text/html"); 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) { function getPlugins(req: Request, res: Response) {
let plugins: lib.PluginWebApi[] = []; let plugins: lib.PluginWebApi[] = [];
for (let pluginInfo of req.app.locals.controller.pluginInfos) { for (let pluginInfo of req.app.locals.controller.pluginInfos) {
@@ -129,6 +137,9 @@ function getPlugins(req: Request, res: Response) {
if (web.main === "remoteEntry.js") { if (web.main === "remoteEntry.js") {
web.error = "Incompatible old remoteEntry.js entrypoint."; 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 }); plugins.push({ name, version: pluginInfo.version, enabled, loaded, web, npmPackage: pluginInfo.npmPackage });
} }
res.send(plugins); res.send(plugins);
+2
View File
@@ -13,7 +13,9 @@ declare module "*.md" {
export default value; export default value;
} }
/** Path the web interface is served under, ends with a slash. */
declare const webRoot: string; declare const webRoot: string;
/** Path static assets are served under, ends with a slash. */
declare const staticRoot: string; declare const staticRoot: string;
declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>; 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; continue;
} }
try { try {
await loadScript(`${webRoot}${meta.web.main}`); await loadScript(`${staticRoot}${meta.web.main}`);
let container: any = (window as { [key: string]: any })[`plugin_${meta.name}`]; let container: any = (window as { [key: string]: any })[`plugin_${meta.name}`];
if (!container) { if (!container) {
throw new Error(`Plugin did not expose its container via plugin_${meta.name}`); 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; return;
} }
let response = await fetch(`${staticRoot}static/${assetFilename}`); let response = await fetch(`${staticRoot}${assetFilename}`);
if (response.ok) { if (response.ok) {
let data = await response.json(); let data = await response.json();
setAssetData(asset === "locale" ? new Map(data) : data); setAssetData(asset === "locale" ? new Map(data) : data);
@@ -21,7 +21,7 @@ class ExportLocaleStore {
return; return;
} }
let response = await fetch(`${staticRoot}static/${cacheEntry.localePath}`); let response = await fetch(`${staticRoot}${cacheEntry.localePath}`);
if (response.ok) { if (response.ok) {
cacheEntry.data = new Map(await response.json() as ExportLocale); cacheEntry.data = new Map(await response.json() as ExportLocale);
for (const callback of cacheEntry.callbacks) { for (const callback of cacheEntry.callbacks) {
@@ -26,7 +26,7 @@ class ExportPrototypeMetadataStore {
return; return;
} }
const response = await fetch(`${staticRoot}static/${cacheEntry.metadataPath}`); const response = await fetch(`${staticRoot}${cacheEntry.metadataPath}`);
if (!response.ok) { if (!response.ok) {
notify( notify(
`Failed to fetch prototype metadata for mod pack ${modPackId}, server returned: ` + `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 metadata = await response.json() as ExportMetadata;
const spriteUrl = `${staticRoot}static/${cacheEntry.spritesheetPath}`; const spriteUrl = `${staticRoot}${cacheEntry.spritesheetPath}`;
const styleContent: string[] = []; const styleContent: string[] = [];
styleContent.push(`\ styleContent.push(`\
.factorio-icon\ .factorio-icon\