Serve hashed assets from R2 via CDN_BASE (#3773)

## Description:

Add an optional CDN_BASE env var that prefixes hashed asset URLs from
asset-manifest.json, so the app can serve static assets from R2/CDN
instead of the app origin. The value is determined at runtime via the
EJS template (window.CDN_BASE) — empty string means "same origin,"
matching today's behavior.

A hack to load the worker bundle:

A same-origin Blob script that dynamic-import()s the cross-origin worker
module and buffers early postMessage calls until the imported module's
handler attaches, sidestepping the browser's refusal to construct a
Worker directly from a cross-origin URL.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2026-04-27 11:27:54 -06:00
committed by GitHub
parent 4aeece4aef
commit 4aa726cfd8
14 changed files with 295 additions and 23 deletions
+36 -7
View File
@@ -6,7 +6,11 @@ import { fileURLToPath } from "url";
import { defineConfig, loadEnv, type Plugin } from "vite";
import { createHtmlPlugin } from "vite-plugin-html";
import tsconfigPaths from "vite-tsconfig-paths";
import { type AssetManifest, buildAssetUrl } from "./src/core/AssetUrls";
import {
type AssetManifest,
buildAssetUrl,
rewriteAssetsForCdn,
} from "./src/core/AssetUrls";
import {
buildPublicAssetManifest,
copyRootPublicFiles,
@@ -57,20 +61,43 @@ export default defineConfig(({ mode }) => {
const assetManifest: AssetManifest = isProduction
? buildPublicAssetManifest(sourceDirs)
: {};
const cdnBase = env.CDN_BASE ?? "";
const htmlAssetData = {
assetManifest: JSON.stringify(assetManifest),
cdnBase: JSON.stringify(cdnBase),
gameEnv: JSON.stringify(env.GAME_ENV ?? "dev"),
manifestHref: buildAssetUrl("manifest.json", assetManifest),
faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest),
manifestHref: buildAssetUrl("manifest.json", assetManifest, cdnBase),
faviconHref: buildAssetUrl("images/Favicon.svg", assetManifest, cdnBase),
gameplayScreenshotUrl: buildAssetUrl(
"images/GameplayScreenshot.png",
assetManifest,
cdnBase,
),
backgroundImageUrl: buildAssetUrl("images/background.webp", assetManifest),
desktopLogoImageUrl: buildAssetUrl("images/OpenFront.png", assetManifest),
mobileLogoImageUrl: buildAssetUrl("images/OF.png", assetManifest),
backgroundImageUrl: buildAssetUrl(
"images/background.webp",
assetManifest,
cdnBase,
),
desktopLogoImageUrl: buildAssetUrl(
"images/OpenFront.png",
assetManifest,
cdnBase,
),
mobileLogoImageUrl: buildAssetUrl("images/OF.png", assetManifest, cdnBase),
};
// Vite's HTML transform replaces the source <script src="/src/client/Main.ts">
// with the hashed bundle URL and injects <link rel="modulepreload"> /
// <link rel="stylesheet"> tags. rewriteAssetsForCdn rewrites those refs to
// an EJS placeholder so RenderHtml.ts can prefix them with CDN_BASE at
// request time.
const injectCdnBaseTemplate = (): Plugin => ({
name: "inject-cdn-base-template",
apply: "build" as const,
enforce: "post",
transformIndexHtml: rewriteAssetsForCdn,
});
let viteBundleFiles: string[] = [];
const syncHashedPublicAssets = (): Plugin => ({
name: "sync-hashed-public-assets",
@@ -157,7 +184,9 @@ export default defineConfig(({ mode }) => {
},
}),
]),
...(isProduction ? [syncHashedPublicAssets()] : []),
...(isProduction
? [injectCdnBaseTemplate(), syncHashedPublicAssets()]
: []),
tailwindcss(),
],