utils/resolveAssetPath.jsjavascript
/**
 * @module utils/resolveAssetPath
 * @description Resolves root-absolute app paths ("/assets/...", "/data/...")
 * against Vite's `base` (import.meta.env.BASE_URL). Vite only rewrites
 * imported/bundled URLs - runtime fetch() calls and path strings inside the
 * content JSONs are served as-is, so under a subpath deployment (e.g. the
 * test server's /<group>/<project>/<release>/) they would hit the domain
 * root and 404. Every runtime path therefore goes through here once.
 */

// trailing slash stripped so `${BASE}${"/assets/x"}` never doubles the "/"
const BASE = (import.meta.env.BASE_URL || "/").replace(/\/$/, "");

/**
 * Prefixes a root-absolute path with the app's base path. Full URLs
 * (http/https/protocol-relative), already-relative paths and non-strings are
 * returned unchanged, as is everything when the app is served from "/".
 *
 * @param {*} path - Candidate path, e.g. "/assets/images/foo.webp".
 * @returns {*} The resolved path, e.g. "/group/project/assets/images/foo.webp".
 */
export const resolveAssetPath = (path) => {
  if (!BASE || typeof path !== "string") return path;
  if (!path.startsWith("/") || path.startsWith("//")) return path;
  return `${BASE}${path}`;
};

/**
 * Recursively resolves every "/assets/..." string inside a content JSON
 * structure via {@link resolveAssetPath}. Applied once by ContentProvider
 * right after fetching, so no consumer (element adapters, thumbnails,
 * screensaver video) needs to resolve paths itself.
 *
 * @param {*} value - Any JSON value (object, array, string, ...).
 * @returns {*} A copy with all asset paths resolved.
 */
export const resolveAssetPathsDeep = (value) => {
  if (typeof value === "string") {
    return value.startsWith("/assets/") ? resolveAssetPath(value) : value;
  }
  if (Array.isArray(value)) return value.map(resolveAssetPathsDeep);
  if (value && typeof value === "object") {
    return Object.fromEntries(
      Object.entries(value).map(([key, entry]) => [key, resolveAssetPathsDeep(entry)])
    );
  }
  return value;
};