mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 07:44:36 +00:00
4aa726cfd8
## 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
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
export type AssetManifest = Record<string, string>;
|
|
|
|
function safeDecodeAssetSegment(segment: string): string {
|
|
try {
|
|
return decodeURIComponent(segment);
|
|
} catch {
|
|
return segment;
|
|
}
|
|
}
|
|
|
|
function assertSafeAssetSegment(segment: string): string {
|
|
const decodedSegment = safeDecodeAssetSegment(segment);
|
|
if (
|
|
segment === "." ||
|
|
segment === ".." ||
|
|
decodedSegment === "." ||
|
|
decodedSegment === ".."
|
|
) {
|
|
throw new Error(`Invalid asset path segment: ${segment}`);
|
|
}
|
|
return decodedSegment;
|
|
}
|
|
|
|
export function encodeAssetPath(path: string): string {
|
|
return normalizeAssetPath(path)
|
|
.split("/")
|
|
.filter((segment) => segment.length > 0)
|
|
.map((segment) => encodeURIComponent(segment))
|
|
.join("/");
|
|
}
|
|
|
|
export function normalizeAssetPath(path: string): string {
|
|
const normalizedPath = path
|
|
.replace(/^\/+/, "")
|
|
.split("/")
|
|
.filter((segment) => segment.length > 0)
|
|
.map((segment) => assertSafeAssetSegment(segment))
|
|
.join("/");
|
|
|
|
if (normalizedPath.length === 0) {
|
|
throw new Error("Asset path must not be empty");
|
|
}
|
|
|
|
return normalizedPath;
|
|
}
|
|
|
|
function isAbsoluteUrl(path: string): boolean {
|
|
return /^https?:\/\//i.test(path);
|
|
}
|
|
|
|
export function buildAssetUrl(
|
|
path: string,
|
|
assetManifest: AssetManifest = {},
|
|
baseUrl: string = "",
|
|
): string {
|
|
if (isAbsoluteUrl(path)) {
|
|
return path;
|
|
}
|
|
|
|
const normalizedPath = normalizeAssetPath(path);
|
|
|
|
const directUrl = assetManifest[normalizedPath];
|
|
if (directUrl) {
|
|
return baseUrl ? `${baseUrl.replace(/\/+$/, "")}${directUrl}` : directUrl;
|
|
}
|
|
|
|
return `/${encodeAssetPath(normalizedPath)}`;
|
|
}
|
|
|
|
declare global {
|
|
var __ASSET_MANIFEST__: AssetManifest | undefined;
|
|
var __CDN_BASE__: string | undefined;
|
|
|
|
interface Window {
|
|
ASSET_MANIFEST?: AssetManifest;
|
|
CDN_BASE?: string;
|
|
}
|
|
}
|
|
|
|
export function getAssetManifest(): AssetManifest {
|
|
if (typeof window !== "undefined" && window.ASSET_MANIFEST !== undefined) {
|
|
return window.ASSET_MANIFEST;
|
|
}
|
|
return globalThis.__ASSET_MANIFEST__ ?? {};
|
|
}
|
|
|
|
// Web workers have no `window`, so they read `__CDN_BASE__` off globalThis,
|
|
// which Worker.worker.ts sets from the init message before any asset fetches.
|
|
// Without this fallback, asset fetches inside workers (e.g. map binaries)
|
|
// would silently bypass the CDN.
|
|
export function getCdnBase(): string {
|
|
if (typeof window !== "undefined" && window.CDN_BASE !== undefined) {
|
|
return window.CDN_BASE;
|
|
}
|
|
return globalThis.__CDN_BASE__ ?? "";
|
|
}
|
|
|
|
export function assetUrl(path: string): string {
|
|
return buildAssetUrl(path, getAssetManifest(), getCdnBase());
|
|
}
|
|
|
|
// Rewrites Vite's emitted /assets/... references in the built index.html to
|
|
// use the cdnBaseRaw EJS placeholder, so RenderHtml.ts can prefix them with
|
|
// CDN_BASE at request time. Scoped to src=/href= attribute values so inline
|
|
// scripts containing the literal "/assets/..." can't be mangled. Does NOT
|
|
// match /_assets/ (underscore) — source-asset manifest URLs are prefixed via
|
|
// buildAssetUrl, not this rewrite. Falls back to "" when cdnBaseRaw is missing
|
|
// so a future renderer that forgets to provide it still produces working
|
|
// same-origin URLs.
|
|
export function rewriteAssetsForCdn(html: string): string {
|
|
return html.replace(
|
|
/(\s(?:src|href)=)(["'])\/assets\//g,
|
|
`$1$2<%- locals.cdnBaseRaw || "" %>/assets/`,
|
|
);
|
|
}
|