mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(themes): serve the Theme Store from GitHub raw, not jsDelivr (#1051)
Theme assets (registry.json, theme CSS, thumbnails) were served from the jsDelivr CDN's mutable @main edge, which caches up to 12h. The registry is purged on every themes push so updates were offered promptly — but the theme CSS could still be served stale, so an update stored pre-update CSS under the new version label and no further update was offered to correct it (a freshly animated theme would install without its @keyframes). Fetch everything from GitHub raw (permissive CORS, ~5-min server cache), which is always current. jsDelivr is dropped entirely: our request volume does not need a CDN, and its only real upside here (high-traffic edge serving) does not apply, while its staleness actively caused the bug. Rename cdnUrl -> assetUrl to reflect the source.
This commit is contained in:
@@ -9,7 +9,7 @@ import type { FetchRegistryResult, Registry, RegistryTheme } from '@/utils/theme
|
|||||||
vi.mock('@/utils/themes/themeRegistry', () => ({
|
vi.mock('@/utils/themes/themeRegistry', () => ({
|
||||||
fetchRegistry: vi.fn(),
|
fetchRegistry: vi.fn(),
|
||||||
fetchThemeCss: vi.fn(async () => 'css'),
|
fetchThemeCss: vi.fn(async () => 'css'),
|
||||||
cdnUrl: (p: string) => `https://cdn.example/${p}`,
|
assetUrl: (p: string) => `https://raw.example/${p}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { fetchRegistry } from '@/utils/themes/themeRegistry';
|
import { fetchRegistry } from '@/utils/themes/themeRegistry';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { formatRelativeTime } from '../../utils/format/relativeTime';
|
|||||||
import { useThemeStore } from '../../store/themeStore';
|
import { useThemeStore } from '../../store/themeStore';
|
||||||
import { useInstalledThemesStore, type InstalledTheme } from '../../store/installedThemesStore';
|
import { useInstalledThemesStore, type InstalledTheme } from '../../store/installedThemesStore';
|
||||||
import {
|
import {
|
||||||
cdnUrl,
|
assetUrl,
|
||||||
fetchRegistry,
|
fetchRegistry,
|
||||||
type RegistryTheme,
|
type RegistryTheme,
|
||||||
} from '../../utils/themes/themeRegistry';
|
} from '../../utils/themes/themeRegistry';
|
||||||
@@ -41,7 +41,7 @@ function pageItemsList(current: number, total: number): (number | 'gap')[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The community Theme Store: browse the jsDelivr-hosted registry, filter by name
|
* The community Theme Store: browse the GitHub-hosted registry, filter by name
|
||||||
* and light/dark, install (fetch + persist + runtime inject), apply, update and
|
* and light/dark, install (fetch + persist + runtime inject), apply, update and
|
||||||
* uninstall. Built-in themes are not in the registry, so they never appear here.
|
* uninstall. Built-in themes are not in the registry, so they never appear here.
|
||||||
*/
|
*/
|
||||||
@@ -82,12 +82,12 @@ export function ThemeStoreSection() {
|
|||||||
.finally(() => { setLoading(false); setRefreshing(false); });
|
.finally(() => { setLoading(false); setRefreshing(false); });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Thumbnails live at a stable CDN path, so the webview caches them hard
|
// GitHub raw caches thumbnails only briefly, but the webview can still hold an
|
||||||
// (jsDelivr sends max-age 7d). Tie a cache-buster to the registry's
|
// old copy; tie a cache-buster to the registry's generatedAt — it changes on
|
||||||
// generatedAt — it changes on every themes push — so refreshed thumbnails
|
// every themes push — so refreshed thumbnails show up after a registry refresh
|
||||||
// show up after a registry refresh instead of being stuck on the old image.
|
// instead of being stuck on the old image.
|
||||||
const thumbUrl = (rel: string) =>
|
const thumbUrl = (rel: string) =>
|
||||||
generatedAt ? `${cdnUrl(rel)}?v=${encodeURIComponent(generatedAt)}` : cdnUrl(rel);
|
generatedAt ? `${assetUrl(rel)}?v=${encodeURIComponent(generatedAt)}` : assetUrl(rel);
|
||||||
|
|
||||||
useEffect(() => { load(false); }, []);
|
useEffect(() => { load(false); }, []);
|
||||||
|
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Theme-Store',
|
themeStoreTitle: 'Theme-Store',
|
||||||
themeStoreSubmitText: 'Eigenes Theme gebaut? Teil es mit der Community — mehr Infos im Themes-Repository.',
|
themeStoreSubmitText: 'Eigenes Theme gebaut? Teil es mit der Community — mehr Infos im Themes-Repository.',
|
||||||
themeStoreSubmitLink: 'Themes-Repository öffnen',
|
themeStoreSubmitLink: 'Themes-Repository öffnen',
|
||||||
themeStoreNetworkNotice: 'Der Theme-Store lädt Katalog und Vorschauen von externen Diensten (jsDelivr-CDN und GitHub). Es werden keine persönlichen Daten gesendet.',
|
themeStoreNetworkNotice: 'Der Theme-Store lädt Katalog und Vorschauen von GitHub. Es werden keine persönlichen Daten gesendet.',
|
||||||
themeStoreStatsNotice: 'Die „Zuletzt geändert“-Daten werden einmal täglich aktualisiert (gegen 04:17 UTC).',
|
themeStoreStatsNotice: 'Die „Zuletzt geändert“-Daten werden einmal täglich aktualisiert (gegen 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Themes suchen…',
|
themeStoreSearchPlaceholder: 'Themes suchen…',
|
||||||
themeStoreFilterMode: 'Nach Modus filtern',
|
themeStoreFilterMode: 'Nach Modus filtern',
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Theme Store',
|
themeStoreTitle: 'Theme Store',
|
||||||
themeStoreSubmitText: 'Made your own theme? Share it with the community — more info in the themes repository.',
|
themeStoreSubmitText: 'Made your own theme? Share it with the community — more info in the themes repository.',
|
||||||
themeStoreSubmitLink: 'Open the themes repository',
|
themeStoreSubmitLink: 'Open the themes repository',
|
||||||
themeStoreNetworkNotice: 'The Theme Store loads its catalogue and previews from external services (the jsDelivr CDN and GitHub). No personal data is sent.',
|
themeStoreNetworkNotice: 'The Theme Store loads its catalogue and previews from GitHub. No personal data is sent.',
|
||||||
themeStoreStatsNotice: 'Last-changed dates refresh once a day (around 04:17 UTC).',
|
themeStoreStatsNotice: 'Last-changed dates refresh once a day (around 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Search themes…',
|
themeStoreSearchPlaceholder: 'Search themes…',
|
||||||
themeStoreFilterMode: 'Filter by mode',
|
themeStoreFilterMode: 'Filter by mode',
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Tienda de temas',
|
themeStoreTitle: 'Tienda de temas',
|
||||||
themeStoreSubmitText: '¿Has creado tu propio tema? Compártelo con la comunidad — más información en el repositorio de temas.',
|
themeStoreSubmitText: '¿Has creado tu propio tema? Compártelo con la comunidad — más información en el repositorio de temas.',
|
||||||
themeStoreSubmitLink: 'Abrir el repositorio de temas',
|
themeStoreSubmitLink: 'Abrir el repositorio de temas',
|
||||||
themeStoreNetworkNotice: 'La tienda de temas carga el catálogo y las vistas previas desde servicios externos (la CDN de jsDelivr y GitHub). No se envían datos personales.',
|
themeStoreNetworkNotice: 'La tienda de temas carga el catálogo y las vistas previas desde GitHub. No se envían datos personales.',
|
||||||
themeStoreStatsNotice: 'Las fechas de última modificación se actualizan una vez al día (sobre las 04:17 UTC).',
|
themeStoreStatsNotice: 'Las fechas de última modificación se actualizan una vez al día (sobre las 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Buscar temas…',
|
themeStoreSearchPlaceholder: 'Buscar temas…',
|
||||||
themeStoreFilterMode: 'Filtrar por modo',
|
themeStoreFilterMode: 'Filtrar por modo',
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Boutique de thèmes',
|
themeStoreTitle: 'Boutique de thèmes',
|
||||||
themeStoreSubmitText: "Vous avez créé votre propre thème ? Partagez-le avec la communauté — plus d'infos dans le dépôt de thèmes.",
|
themeStoreSubmitText: "Vous avez créé votre propre thème ? Partagez-le avec la communauté — plus d'infos dans le dépôt de thèmes.",
|
||||||
themeStoreSubmitLink: 'Ouvrir le dépôt de thèmes',
|
themeStoreSubmitLink: 'Ouvrir le dépôt de thèmes',
|
||||||
themeStoreNetworkNotice: 'Le Theme Store charge son catalogue et ses aperçus depuis des services externes (le CDN jsDelivr et GitHub). Aucune donnée personnelle n’est envoyée.',
|
themeStoreNetworkNotice: 'Le Theme Store charge son catalogue et ses aperçus depuis GitHub. Aucune donnée personnelle n’est envoyée.',
|
||||||
themeStoreStatsNotice: 'Les dates de dernière modification sont actualisées une fois par jour (vers 04h17 UTC).',
|
themeStoreStatsNotice: 'Les dates de dernière modification sont actualisées une fois par jour (vers 04h17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Rechercher des thèmes…',
|
themeStoreSearchPlaceholder: 'Rechercher des thèmes…',
|
||||||
themeStoreFilterMode: 'Filtrer par mode',
|
themeStoreFilterMode: 'Filtrer par mode',
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Temabutikk',
|
themeStoreTitle: 'Temabutikk',
|
||||||
themeStoreSubmitText: 'Laget ditt eget tema? Del det med fellesskapet — mer info i tema-repoet.',
|
themeStoreSubmitText: 'Laget ditt eget tema? Del det med fellesskapet — mer info i tema-repoet.',
|
||||||
themeStoreSubmitLink: 'Åpne tema-repoet',
|
themeStoreSubmitLink: 'Åpne tema-repoet',
|
||||||
themeStoreNetworkNotice: 'Theme Store laster katalogen og forhåndsvisninger fra eksterne tjenester (jsDelivr-CDN og GitHub). Ingen personopplysninger sendes.',
|
themeStoreNetworkNotice: 'Theme Store laster katalogen og forhåndsvisninger fra GitHub. Ingen personopplysninger sendes.',
|
||||||
themeStoreStatsNotice: 'Sist endret-datoene oppdateres én gang om dagen (rundt 04:17 UTC).',
|
themeStoreStatsNotice: 'Sist endret-datoene oppdateres én gang om dagen (rundt 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Søk i temaer…',
|
themeStoreSearchPlaceholder: 'Søk i temaer…',
|
||||||
themeStoreFilterMode: 'Filtrer etter modus',
|
themeStoreFilterMode: 'Filtrer etter modus',
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Themawinkel',
|
themeStoreTitle: 'Themawinkel',
|
||||||
themeStoreSubmitText: 'Eigen thema gemaakt? Deel het met de community — meer info in de themarepository.',
|
themeStoreSubmitText: 'Eigen thema gemaakt? Deel het met de community — meer info in de themarepository.',
|
||||||
themeStoreSubmitLink: 'Themarepository openen',
|
themeStoreSubmitLink: 'Themarepository openen',
|
||||||
themeStoreNetworkNotice: 'De Theme Store laadt de catalogus en previews van externe diensten (de jsDelivr-CDN en GitHub). Er worden geen persoonlijke gegevens verzonden.',
|
themeStoreNetworkNotice: 'De Theme Store laadt de catalogus en previews van GitHub. Er worden geen persoonlijke gegevens verzonden.',
|
||||||
themeStoreStatsNotice: 'De laatst-gewijzigd-datums worden eenmaal per dag bijgewerkt (rond 04:17 UTC).',
|
themeStoreStatsNotice: 'De laatst-gewijzigd-datums worden eenmaal per dag bijgewerkt (rond 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: "Thema's zoeken…",
|
themeStoreSearchPlaceholder: "Thema's zoeken…",
|
||||||
themeStoreFilterMode: 'Filteren op modus',
|
themeStoreFilterMode: 'Filteren op modus',
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Magazin de teme',
|
themeStoreTitle: 'Magazin de teme',
|
||||||
themeStoreSubmitText: 'Ți-ai creat propria temă? Împărtășește-o cu comunitatea — mai multe informații în depozitul de teme.',
|
themeStoreSubmitText: 'Ți-ai creat propria temă? Împărtășește-o cu comunitatea — mai multe informații în depozitul de teme.',
|
||||||
themeStoreSubmitLink: 'Deschide depozitul de teme',
|
themeStoreSubmitLink: 'Deschide depozitul de teme',
|
||||||
themeStoreNetworkNotice: 'Theme Store încarcă catalogul și previzualizările de la servicii externe (CDN-ul jsDelivr și GitHub). Nu se trimit date personale.',
|
themeStoreNetworkNotice: 'Theme Store încarcă catalogul și previzualizările de la GitHub. Nu se trimit date personale.',
|
||||||
themeStoreStatsNotice: 'Datele privind ultima modificare se actualizează o dată pe zi (în jurul orei 04:17 UTC).',
|
themeStoreStatsNotice: 'Datele privind ultima modificare se actualizează o dată pe zi (în jurul orei 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Caută teme…',
|
themeStoreSearchPlaceholder: 'Caută teme…',
|
||||||
themeStoreFilterMode: 'Filtrează după mod',
|
themeStoreFilterMode: 'Filtrează după mod',
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ export const settings = {
|
|||||||
themeStoreTitle: 'Магазин тем',
|
themeStoreTitle: 'Магазин тем',
|
||||||
themeStoreSubmitText: 'Создали свою тему? Поделитесь с сообществом — подробности в репозитории тем.',
|
themeStoreSubmitText: 'Создали свою тему? Поделитесь с сообществом — подробности в репозитории тем.',
|
||||||
themeStoreSubmitLink: 'Открыть репозиторий тем',
|
themeStoreSubmitLink: 'Открыть репозиторий тем',
|
||||||
themeStoreNetworkNotice: 'Магазин тем загружает каталог и превью с внешних сервисов (CDN jsDelivr и GitHub). Личные данные не отправляются.',
|
themeStoreNetworkNotice: 'Магазин тем загружает каталог и превью с GitHub. Личные данные не отправляются.',
|
||||||
themeStoreStatsNotice: 'Даты последнего изменения обновляются раз в день (около 04:17 UTC).',
|
themeStoreStatsNotice: 'Даты последнего изменения обновляются раз в день (около 04:17 UTC).',
|
||||||
themeStoreSearchPlaceholder: 'Поиск тем…',
|
themeStoreSearchPlaceholder: 'Поиск тем…',
|
||||||
themeStoreFilterMode: 'Фильтр по режиму',
|
themeStoreFilterMode: 'Фильтр по режиму',
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ export const settings = {
|
|||||||
themeStoreTitle: '主题商店',
|
themeStoreTitle: '主题商店',
|
||||||
themeStoreSubmitText: '做了自己的主题?与社区分享——更多信息见主题仓库。',
|
themeStoreSubmitText: '做了自己的主题?与社区分享——更多信息见主题仓库。',
|
||||||
themeStoreSubmitLink: '打开主题仓库',
|
themeStoreSubmitLink: '打开主题仓库',
|
||||||
themeStoreNetworkNotice: '主题商店从外部服务(jsDelivr CDN 和 GitHub)加载目录和预览。不会发送任何个人数据。',
|
themeStoreNetworkNotice: '主题商店从 GitHub 加载目录和预览。不会发送任何个人数据。',
|
||||||
themeStoreStatsNotice: '最后更新时间每天更新一次(约 04:17 UTC)。',
|
themeStoreStatsNotice: '最后更新时间每天更新一次(约 04:17 UTC)。',
|
||||||
themeStoreSearchPlaceholder: '搜索主题…',
|
themeStoreSearchPlaceholder: '搜索主题…',
|
||||||
themeStoreFilterMode: '按模式筛选',
|
themeStoreFilterMode: '按模式筛选',
|
||||||
|
|||||||
@@ -85,30 +85,13 @@ describe('fetchRegistry', () => {
|
|||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('force-refresh tries GitHub raw first', async () => {
|
it('fetches from GitHub raw, never a CDN', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
vi.stubGlobal('fetch', vi.fn(async (url: string) => { calls.push(url); return okRes(reg('forced')); }));
|
vi.stubGlobal('fetch', vi.fn(async (url: string) => { calls.push(url); return okRes(reg('fresh')); }));
|
||||||
|
|
||||||
await fetchRegistry({ force: true });
|
await fetchRegistry({ force: true });
|
||||||
expect(calls[0]).toContain('raw.githubusercontent.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('force-refresh falls back to jsDelivr when raw fails', async () => {
|
|
||||||
const fetchMock = vi.fn(async (url: string) =>
|
|
||||||
url.includes('raw.githubusercontent.com') ? failRes() : okRes(reg('cdn')));
|
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
|
||||||
|
|
||||||
const r = await fetchRegistry({ force: true });
|
|
||||||
expect(r.registry.generatedAt).toBe('cdn');
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('non-force load uses the CDN only (not raw)', async () => {
|
|
||||||
const calls: string[] = [];
|
|
||||||
vi.stubGlobal('fetch', vi.fn(async (url: string) => { calls.push(url); return okRes(reg('cdn')); }));
|
|
||||||
|
|
||||||
await fetchRegistry();
|
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
expect(calls[0]).not.toContain('raw.githubusercontent.com');
|
expect(calls[0]).toContain('raw.githubusercontent.com');
|
||||||
|
expect(calls[0]).not.toContain('jsdelivr');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Theme Store registry client. Reads the auto-generated `registry.json` from the
|
* Theme Store registry client. Reads the auto-generated `registry.json` and each
|
||||||
* public `Psysonic/psysonic-themes` repo via the jsDelivr CDN (CORS-enabled,
|
* theme's CSS/thumbnail straight from the public `Psysonic/psysonic-themes` repo
|
||||||
* globally cached). The registry is cached in localStorage with a TTL so the
|
* over GitHub raw (permissive CORS, ~5-minute server cache). The registry is
|
||||||
* store opens instantly and works offline against the last-seen catalogue.
|
* cached in localStorage with a TTL so the store opens instantly and works
|
||||||
|
* offline against the last-seen catalogue.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CDN_BASE = 'https://cdn.jsdelivr.net/gh/Psysonic/psysonic-themes@main';
|
const RAW_BASE = 'https://raw.githubusercontent.com/Psysonic/psysonic-themes/main';
|
||||||
const REGISTRY_URL = `${CDN_BASE}/registry.json`;
|
const REGISTRY_URL = `${RAW_BASE}/registry.json`;
|
||||||
// GitHub raw serves with a ~5-minute cache (vs jsDelivr's up-to-12h @main edge)
|
|
||||||
// and permissive CORS. Used only on a manual refresh so freshly merged themes
|
|
||||||
// appear without waiting on — or purging — the shared CDN edge.
|
|
||||||
const RAW_REGISTRY_URL = 'https://raw.githubusercontent.com/Psysonic/psysonic-themes/main/registry.json';
|
|
||||||
const CACHE_KEY = 'psysonic_theme_registry_cache';
|
const CACHE_KEY = 'psysonic_theme_registry_cache';
|
||||||
const TTL_MS = 12 * 60 * 60 * 1000; // 12h — matches jsDelivr's @main edge cache
|
// Client-side cache lifetime: the store opens from this copy without a network
|
||||||
|
// round-trip and falls back to it when offline. The manual refresh button
|
||||||
|
// bypasses it, and GitHub raw is itself fresh (~5-min server cache).
|
||||||
|
const TTL_MS = 12 * 60 * 60 * 1000; // 12h
|
||||||
|
|
||||||
export interface RegistryTheme {
|
export interface RegistryTheme {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -45,9 +45,9 @@ interface CacheEnvelope {
|
|||||||
registry: Registry;
|
registry: Registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Absolute CDN URL for a repo-relative path (css / thumbnail). */
|
/** Absolute GitHub-raw URL for a repo-relative asset path (css / thumbnail). */
|
||||||
export function cdnUrl(relPath: string): string {
|
export function assetUrl(relPath: string): string {
|
||||||
return `${CDN_BASE}/${relPath}`;
|
return `${RAW_BASE}/${relPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCache(): CacheEnvelope | null {
|
function readCache(): CacheEnvelope | null {
|
||||||
@@ -91,28 +91,29 @@ export async function fetchRegistry(opts?: { force?: boolean }): Promise<FetchRe
|
|||||||
const cached = readCache();
|
const cached = readCache();
|
||||||
if (cached && Date.now() - cached.ts < TTL_MS) return { registry: cached.registry, stale: false };
|
if (cached && Date.now() - cached.ts < TTL_MS) return { registry: cached.registry, stale: false };
|
||||||
}
|
}
|
||||||
// On a manual refresh, try GitHub raw first (fresher) then fall back to the
|
try {
|
||||||
// jsDelivr CDN; normal loads use the CDN only.
|
const res = await fetch(REGISTRY_URL, { cache: 'no-cache' });
|
||||||
const sources = opts?.force ? [RAW_REGISTRY_URL, REGISTRY_URL] : [REGISTRY_URL];
|
if (res.ok) {
|
||||||
for (const url of sources) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, { cache: 'no-cache' });
|
|
||||||
if (!res.ok) continue;
|
|
||||||
const registry = (await res.json()) as Registry;
|
const registry = (await res.json()) as Registry;
|
||||||
writeCache(registry);
|
writeCache(registry);
|
||||||
return { registry, stale: false };
|
return { registry, stale: false };
|
||||||
} catch {
|
|
||||||
// try the next source
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to the cached copy below
|
||||||
}
|
}
|
||||||
const cached = readCache();
|
const cached = readCache();
|
||||||
if (cached) return { registry: cached.registry, stale: true };
|
if (cached) return { registry: cached.registry, stale: true };
|
||||||
throw new Error('registry fetch failed');
|
throw new Error('registry fetch failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetch a single theme's CSS text from the CDN (repo-relative path). */
|
/**
|
||||||
|
* Fetch a single theme's CSS text from GitHub raw (repo-relative path). Raw is
|
||||||
|
* used rather than a mutable CDN edge so an install or update always gets the
|
||||||
|
* current bytes: a stale edge would otherwise store pre-update CSS under the new
|
||||||
|
* version label, leaving the theme wrong with no further update to correct it.
|
||||||
|
*/
|
||||||
export async function fetchThemeCss(relPath: string): Promise<string> {
|
export async function fetchThemeCss(relPath: string): Promise<string> {
|
||||||
const res = await fetch(cdnUrl(relPath), { cache: 'no-cache' });
|
const res = await fetch(assetUrl(relPath), { cache: 'no-cache' });
|
||||||
if (!res.ok) throw new Error(`theme css fetch failed: ${res.status}`);
|
if (!res.ok) throw new Error(`theme css fetch failed: ${res.status}`);
|
||||||
return res.text();
|
return res.text();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user