From 2f4e0c441852b9597ba80f4acf353e4154aae4c4 Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:50:08 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 10 ++++++++++ packages/controller/src/Controller.ts | 5 +++-- packages/controller/src/routes.ts | 11 +++++++++++ packages/web_ui/global.d.ts | 2 ++ packages/web_ui/src/bootstrap.tsx | 2 +- packages/web_ui/src/components/ModPackViewPage.tsx | 2 +- packages/web_ui/src/store/export_locale_stores.ts | 2 +- .../src/store/export_prototype_metadata_store.ts | 4 ++-- 8 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c13d77..edfbb13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,16 @@ Many thanks to the following for contributing to this release: [@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]() + +### 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]() + ## Version 2.0.0-alpha.27 ### Features diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index bbf571d8..cd63d9cf 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -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"); diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index 4622502b..bcd3eaaa 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -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); diff --git a/packages/web_ui/global.d.ts b/packages/web_ui/global.d.ts index 475b5037..90fd88c1 100644 --- a/packages/web_ui/global.d.ts +++ b/packages/web_ui/global.d.ts @@ -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; diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index 8ef588bd..2695d154 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -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}`); diff --git a/packages/web_ui/src/components/ModPackViewPage.tsx b/packages/web_ui/src/components/ModPackViewPage.tsx index c00a4315..19c8ae53 100644 --- a/packages/web_ui/src/components/ModPackViewPage.tsx +++ b/packages/web_ui/src/components/ModPackViewPage.tsx @@ -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); diff --git a/packages/web_ui/src/store/export_locale_stores.ts b/packages/web_ui/src/store/export_locale_stores.ts index 8d3c6d17..e15c6aa8 100644 --- a/packages/web_ui/src/store/export_locale_stores.ts +++ b/packages/web_ui/src/store/export_locale_stores.ts @@ -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) { diff --git a/packages/web_ui/src/store/export_prototype_metadata_store.ts b/packages/web_ui/src/store/export_prototype_metadata_store.ts index 59169fd3..cd919658 100644 --- a/packages/web_ui/src/store/export_prototype_metadata_store.ts +++ b/packages/web_ui/src/store/export_prototype_metadata_store.ts @@ -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\